# Lido Documentation - Full Content > Documentation for the Lido liquid staking protocol on Ethereum and L2s. Covers protocol contracts, integrations, node operator guides, CSM, stVaults, and the Earn product. --- ## Section: Main Docs --- # Introduction This documentation is intended to introduce the general user to the project, as well as to serve as a guide for anyone who may be developing software using Lido.  โ” Get started with [FAQ](https://lido.fi/faq)  ๐Ÿ–ฅ See guides for Node Operators, Vault managers or Oracle Operators at [Run on Lido](/run-on-lido/intro)  ๐Ÿž Follow the [bug bounty](https://immunefi.com/bounty/lido/) program  ๐Ÿ’ฐ Access grants with [LEGO](https://lego.lido.fi)  ๐ŸŒ Everything about [Lido Node Operators](https://operatorportal.lido.fi/)  ๐Ÿ”— Integrate your DApp following this [guide](/guides/lido-tokens-integration-guide)  ๐Ÿ”ˆ Participate in [governance](https://research.lido.fi/) forum  ๐Ÿท๏ธ Audit [source code](https://github.com/lidofinance)  ๐Ÿค Support, partnerships and more in [Discord](https://discord.com/invite/lido), [Telegram](https://t.me/lidofinance)  โœ… Updates in [blog](https://blog.lido.fi/) and [X/Twitter](https://x.com/lidofinance)  โ„น๏ธ Find support at [help center](https://help.lido.fi/) ## What is Lido Lido is the leading liquid staking solution - providing a simple way to get rewards on your digital tokens. By staking with Lido your tokens remain liquid and can be used across a range of DeFi applications, getting extra rewards. 1. **Staking pool**. Protocol to manage deposits, staking rewards, and withdrawals. A separate one for every supported network. 2. `st[token]`. Unlike staked tokens, the Lido `st[token]` are freely transferable instead of locked as in the case of native staking. Lido lets users operate with staked tokens by leveraging collateral, lending, farming, and other kinds of DeFi protocols. 3. **DAO**. Lido liquid protocols management entity, responsible for picking node operators, configuring the protocol parameters and much more. > ๐Ÿ“ To dive into the details of governance design and implementation, proceed to [DAO](lido-dao.md) 4. **Node Operators**. Entities that manage a secure and stable infrastructure for running validator clients for the benefit of the protocols. Theyโ€™re dedicated staking providers who can ensure the safety of funds belonging to the protocol users and correctness of validator operations. Ethereum is and remains a primary focus of Lido. Thatโ€™s affected the organisational structure - the governance is implemented through ERC20 `LDO` token on Ethereum. ## Liquid staking ### Problem statement Traditionally, staking in Proof-of-Stake (PoS) protocol based projects has been about locking oneโ€™s tokens in one project for a long time and expecting a fixed, predetermined staking reward in return. While it guarantees the return on staked tokens much like a bond, it also limits the opportunities of generating higher returns on those tokens from the DeFi ecosystem. If youโ€™ve staked all of your crypto holdings, you canโ€™t invest or trade in more profitable crypto pairs on exchanges. ### Solution Liquid staking allows using the `st[token]` in other trading opportunities to let the user get the best of both worlds - a reward on your staked tokens, as well as the returns from new trading opportunities. Liquid staking introduces various fundamental benefits by: - Making staking process simple - no need to worry about hardware setup and maintenance; - Making it possible to get rewards on as small a deposit as users want (i.e, Ethereum requires minimum 32 `ETH` staked); - Providing the `st[token]` a building block for other applications and protocols (e.g., as collateral in lending or other trading DeFi solutions). Liquid staking gives an opportunity to maximize the potential while having the best of both worlds; - Providing an alternative to or even encompassing exchange staking, solo staking, and other semi-custodial and decentralised protocols. ### Comparison with other staking options **Solo staking** is great, but it comes with some disadvantages. Setting up a validator node requires a pristine technical understanding, brings with it a minimum deposit of 32 ETH in Ethereum case, slashing and offline penalties can get very severe if the staking is managed improperly and finally the staked amount is locked up for a significant period. Solo staking is similar to other option **SaaS staking** in that you are having your own validator keys. Nevertheless, with SaaS you must trust a third-party (usually centralised), which may act maliciously, attacked or simply regulated. Going back to the Ethereum example, there remains a requirement for a minimum amount of 32 `ETH` staked. Alternatively, it may be possible to produce staking through **centralised exchanges**. Needless to say, crypto tokens and CeFi are not suited well together from the fundamental standpoint. It is also worth mentioning the economic aspect - by staking within some centralised entities, the user does not receive a corresponding token in return and, thus, loses the opportunity to perform any subsequent activity within DeFi or the same centralised entity, where tokens were staked. Yes, APR, when staking on centralised exchanges might be higher, but with a significant amount aggregated within the centralised entity comes a huge potential influence to the ecosystem that was fundamentally designed decentralised. Through the use of a **liquid staking** solution such as Lido, users can eliminate these inconveniences and benefit from non-custodial staking backed by the actively maintained validators set. Liquid staking is unlocking the potential of PoS by giving users the ability to not only stake their tokens, but have the liquidity to use those tokens in DeFi projects that way not only increasing rewards for the individual, but growing the staking participation in general. ## Lido on Ethereum APR :::note APR provides only the current estimation of the rewards without any upfront forecasts. ::: > User's APR (Lido staking APR) = Protocol APR * (1 - Protocol fee) ### Protocol APR By Protocol APR we mean gross annual percentage rate โ€” the overall Consensus Layer (CL) and Execution Layer (EL) rewards received by Lido validators to total pooled ETH estimated as moving average of the last 7 days. ### Consensus Layer Validators receive rewards when they perform consensus layer validatorsโ€™ duties: - attest blocks - propose blocks - participate in sync committees. The value of the rewards in each epoch is calculated from a base reward. This is the base unit representing the average reward received by a validator under optimal conditions per epoch. Base rewards are inversely proportional to the square root of the total staked ether in Ethereum, that's why the amount of reward per validator decreases as the overall number of validators on Ethereum grows. In addition to rewards, penalties and slashing can be applied to validators on the CL. A penalty is a form of reducing the validatorโ€™s stake, which is not online or doesnโ€™t meet certain specifications criteria when attesting the blocks. Being slashed means that the misbehaving validator is forced to exit the beacon chain at a point in the future, receiving a [number of penalties until it leaves](https://docs.prylabs.network/docs/how-prysm-works/validator-lifecycle#slashing-state). As a result of it, APR can also reduce. > **Performance of Lido validators** The better the underlying operator sets are, the more robust, resilient, and performant the underlying protocol. Check [Operator Statistics and Metrics](https://operatorportal.lido.fi/operator-statistics-and-metrics). Learn more about rewards and penalties - [ethereum.org](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/rewards-and-penalties/) ### Execution Layer Validators began to receive EL rewards after [the Merge](https://ethereum.org/en/roadmap/merge/). EL rewards consist of four parts: - **Priority fee** Priority fee refers to optional additional fees (for example it may refer to EIP-1559) paid directly to validators in order to incentivize them to include the given transaction in a block. Also it depends on network demand: as higher network demand, as higher could be priority fee. - **Maximal Extractable Value (MEV) rewards** MEV refers to the maximum value that can be extracted by the validator from block production in excess of the standard block reward and gas fees by including, excluding, and changing the order of transactions in a block. ### Rewards socialization model With Lido, you receive staking rewards within 24 hours of your deposit being made, without waiting for validator activation. ### Protocol fee > Lido applies a 10% fee on staking rewards that are split between node operators and the DAO Treasury. The fee can be changed by the DAO pending a successful vote. ### More about APR calculation Please refer to the [API doc page](/integrations/api#simple-moving-average-lido-apr-for-7-last-days) for further details. ## Security The security of Lido is highest priority beginning at the time of its initial deployment, but still users should investigate risks involved with Lido before engaging with it. We are constantly working on security improvements: - Using of DAO for governance decisions & to manage risk factors. - Having multiple audits finished (see [more](https://github.com/lidofinance/audits)). - Having a committee of elected, best-in-class validators to minimise staking risk. - Allowing staking across multiple validators (i.e. current Ethereum staking mechanism only allows to choose only a single validator) - Using of non-custodial staking service to eliminate counterparty risk. In the [scorecard](https://scorecard.lido.fi/) you may find an outlined set of attributes that we think are important for the decentralisation of the protocol, and how Lido is faring against these targets. > ๐Ÿ“ As for now, scorecard has only Ethereum related content. --- # Accounting - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/Accounting.sol) - [Deployed contract](https://etherscan.io/address/0x23ED611be0e1a820978875C0122F92260804cdDf) Handles oracle reports and calculates protocol state changes including rebases, fee distribution, and stVault bad debt internalization. ## What is Accounting? Accounting is the core contract that processes oracle reports for Lido: - receives oracle reports from `AccountingOracle` - calculates share rate changes and token rebases - distributes protocol fees to staking modules and treasury - finalizes withdrawal requests - internalizes bad debt from stVaults via `VaultHub` - notifies external contracts about rebases The contract acts as the central point for all accounting operations. ## How it works 1. `HashConsensus` reaches consensus on the accounting report hash. 2. `AccountingOracle.submitReportData()` validates the sender, contract version, consensus version, and report hash. 3. `AccountingOracle` pre-validates the per-module validator balances and the CL balance change rates via `StakingRouter.validateReportValidatorBalancesByStakingModule()` and `OracleReportSanityChecker.checkModuleAndCLBalancesChangeRates()`. 4. `AccountingOracle` reports exited validator counts to `StakingRouter` and checks them via `OracleReportSanityChecker.checkExitedValidatorsCount()`. 5. `AccountingOracle` reports per-module validator balances to `StakingRouter` via `reportValidatorBalancesByStakingModule()` โ€” these are used as the basis for rewards distribution. 6. `AccountingOracle` calls `WithdrawalQueue.onOracleReport()` to update bunker mode and timing bounds. 7. `AccountingOracle` calls `Accounting.handleOracleReport()`. 8. Accounting snapshots pre-report state (including `Lido.getBalanceStats()` and the bad debt to internalize from `VaultHub`) and simulates the report (including `WithdrawalQueue.prefinalize()` and `OracleReportSanityChecker.smoothenTokenRebase()`). 9. Accounting runs sanity checks via `OracleReportSanityChecker.checkAccountingOracleReport()`. 10. Accounting calls `Burner.requestBurnShares()` to lock shares for withdrawal finalization (if needed). 11. Accounting updates CL state via `Lido.processClStateUpdate()`. 12. Accounting internalizes bad debt via `VaultHub.decreaseInternalizedBadDebt()` and `Lido.internalizeExternalBadDebt()`. 13. Accounting calls `Burner.commitSharesToBurn()` (if needed). 14. Accounting calls `Lido.collectRewardsAndProcessWithdrawals()` to process withdrawals and rewards. 15. If fees are due, Accounting mints fee shares, distributes them, and calls `StakingRouter.reportRewardsMinted()`. 16. Accounting notifies the post-rebase receiver and calls `Lido.emitTokenRebase()`. 17. `AccountingOracle` updates `LazyOracle.updateReportData()` and stores extra-data processing state. ```mermaid graph TB; HC[HashConsensus]-->AO[AccountingOracle]; AO-->SR[StakingRouter: exited validators & validator balances]; AO-->WQ[WithdrawalQueue: onOracleReport]; AO-->A[Accounting: handleOracleReport]; A-->SC[OracleReportSanityChecker]; A-->B[Burner: requestBurnShares/commitSharesToBurn]; A-->L[Lido: processClStateUpdate/collectRewardsAndProcessWithdrawals/emitTokenRebase]; A-->VH[VaultHub: decreaseInternalizedBadDebt]; A-->SR; AO-->LO[LazyOracle: updateReportData]; ``` ## Structs ### ReportValues Oracle report input data (defined in `contracts/common/interfaces/ReportValues.sol`): ```solidity struct ReportValues { uint256 timestamp; // Block timestamp when the report is based uint256 timeElapsed; // Duration since the previous report uint256 clValidatorsBalance; // Balance of Lido validators on CL, excluding pending deposits uint256 clPendingBalance; // Balance of Lido-attributed pending deposits on CL uint256 withdrawalVaultBalance; // Current withdrawal vault holdings uint256 elRewardsVaultBalance; // Execution Layer rewards vault holdings uint256 sharesRequestedToBurn; // stETH shares marked for burning via Burner uint256[] withdrawalFinalizationBatches; // Sorted array of withdrawal request IDs uint256 simulatedShareRate; // Projected share rate value } ``` ### PreReportState Snapshot of protocol state before report processing (internal struct): ```solidity struct PreReportState { uint256 clValidatorsBalance; // CL validators balance (excluding pending deposits) at the last report uint256 clPendingBalance; // CL pending deposits balance at the last report uint256 depositedBalance; // Ether deposited since the last report, as of the reporting refSlot uint256 totalPooledEther; // Total pooled ether before report uint256 totalShares; // Total shares before report uint256 externalShares; // Shares backed by external vaults uint256 externalEther; // Ether in external vaults uint256 badDebtToInternalize; // Bad debt amount to internalize this report } ``` ### CalculatedValues Computed state changes from a report: ```solidity struct CalculatedValues { uint256 withdrawalsVaultTransfer; // ETH to transfer from withdrawal vault uint256 elRewardsVaultTransfer; // ETH to transfer from EL rewards vault uint256 etherToFinalizeWQ; // ETH needed to finalize withdrawal queue uint256 sharesToFinalizeWQ; // Shares to finalize withdrawal queue uint256 sharesToBurnForWithdrawals; // Shares to burn for withdrawals uint256 totalSharesToBurn; // Total shares to be burned uint256 sharesToMintAsFees; // Shares to mint as protocol fees FeeDistribution feeDistribution; // Fee distribution details uint256 principalClBalance; // CL balances at the previous report plus deposits made since then uint256 preTotalShares; // Total shares before update uint256 preTotalPooledEther; // Total pooled ETH before update uint256 postInternalShares; // Internal shares after update uint256 postInternalEther; // Internal ETH after update uint256 postTotalShares; // Total shares after update uint256 postTotalPooledEther; // Total pooled ETH after update } ``` ### FeeDistribution Protocol fee allocation: ```solidity struct FeeDistribution { address[] moduleFeeRecipients; // Addresses receiving module fees uint256[] moduleIds; // IDs of staking modules uint256[] moduleSharesToMint; // Shares to mint for each module uint256 treasurySharesToMint; // Shares to mint for treasury } ``` ## View methods ### simulateOracleReport(ReportValues \_report) ```solidity function simulateOracleReport( ReportValues calldata _report ) external view returns (CalculatedValues memory) ``` Simulates an oracle report without applying changes. Returns calculated state changes that would result from the report. Used by oracle daemons to compute the simulated share rate before submitting. Note: For simulation, uses `vaultHub.badDebtToInternalize()` to fetch the current bad debt value, whereas actual reports use `badDebtToInternalizeForLastRefSlot()`. ## Methods ### handleOracleReport(ReportValues \_report) ```solidity function handleOracleReport(ReportValues calldata _report) external ``` Handles an oracle report and applies all calculated state changes to the protocol. Can only be called by the `AccountingOracle` contract. The method performs these operations in order: 1. Runs sanity checks on report data. 2. Requests `Burner.requestBurnShares()` for withdrawal queue finalization (if applicable). 3. Updates consensus layer state on Lido via `processClStateUpdate()`. 4. Internalizes bad debt (calls `VaultHub.decreaseInternalizedBadDebt()` and `Lido.internalizeExternalBadDebt()`). 5. Commits shares to burn via `Burner.commitSharesToBurn()`. 6. Collects EL rewards and processes withdrawals via `Lido.collectRewardsAndProcessWithdrawals()`. 7. If fees are due: mints fee shares, distributes fees, then calls `StakingRouter.reportRewardsMinted()`. 8. Notifies rebase observers via `handlePostTokenRebase()`. 9. Emits token rebase event via `emitTokenRebase()`. ## Errors ```solidity error NotAuthorized(string operation, address addr); error IncorrectReportTimestamp(uint256 reportTimestamp, uint256 upperBoundTimestamp); error InternalSharesCantBeZero(); ``` ## Related - [AccountingOracle](/contracts/accounting-oracle) - [Lido](/contracts/lido) - [VaultHub](/contracts/vault-hub) - [OracleReportSanityChecker](/contracts/oracle-report-sanity-checker) - [WithdrawalQueue](/contracts/withdrawal-queue-erc721) --- # AccountingOracle - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/oracle/AccountingOracle.sol) - [Deployed contract](https://etherscan.io/address/0x852deD011285fe67063a08005c71a85690503Cee) - Inherits [BaseOracle](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/oracle/BaseOracle.sol) :::info It's advised to read [What is Lido Oracle mechanism](/guides/oracle-operator-manual#intro) before ::: ## What is AccountingOracle AccountingOracle is a contract that collects information submitted by off-chain oracles about the balances of Lido-participating validators โ€” both active on the Consensus Layer and pending in its deposit queue โ€” including the per-staking-module breakdown; the amounts of funds accumulated in the protocol vaults (i.e., [withdrawal](/contracts/withdrawal-vault) and [execution layer rewards](/contracts/lido-execution-layer-rewards-vault) vaults); the number of [exited](/contracts/staking-router#exited-validators) validators; the number of [withdrawal requests](/contracts/withdrawal-queue-erc721#request) the protocol is able to process; and it coordinates the distribution of node operator rewards. The report is applied by the [Accounting](/contracts/accounting) contract, which performs the core state updates and rebase calculations. ## Report cycle The oracle work is delineated by equal time periods called frames. In normal operation, oracles finalize a report in each frame (the frame duration is 225 Ethereum Consensus Layer epochs, each frame starts at ~12:00 noon UTC). Each frame has a reference slot and processing deadline. Report data is gathered by looking at the world state (both Ethereum Execution and Consensus Layers) at the moment of the frame's reference slot (including any state changes made in that slot), and must be processed before the frame's processing deadline. Reference slot for each frame is set to the last slot of the epoch preceding the frame's first epoch. The processing deadline is set to the last slot of the last epoch of the frame. Note: the frame length [can be changed](/contracts/hash-consensus#setframeconfig). If an oracle report is delayed, it does not extend the reporting period unless the report is missed; in that case, the next report will cover a longer period. The frame includes these stages: - **Waiting:** the oracle runs as a [daemon](/guides/oracle-operator-manual#the-oracle-daemon) and wakes up every 12 seconds (by default) to find the last finalized slot, trying to align it with the expected reference slot; - **Data collection:** oracles monitor the state of both the execution and consensus layers and collect the data for the successfully arrived finalized reference slot; - **Hash consensus:** oracles analyze the data, compile the report and submit its hash to the [`HashConsensus`](/contracts/hash-consensus) smart contract; - **Core update report:** once the [quorum](/contracts/hash-consensus#getquorum) of hashes is reached, meaning more than half of the oracles submitted the same hash (i.e., 5 of 9 oracle committee members at the moment of writing), one of the oracles chosen in turn submits the actual report to the `AccountingOracle` contract. This triggers the core protocol state update, including the token rebase, distribution of node operator rewards, finalization of withdrawal requests, and the protocol mode decision: whether to enter bunker mode. - **Extra data report:** an additional report carrying information that is not vital for the core update is submitted to AccountingOracle; it can be submitted in chunks (e.g., node operator key states and reward distribution data). :::note As it was said, daily oracle reports shouldn't be taken for granted. Oracle daemons could stop pushing their reports for extended periods of time in case of no [finality](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/#finality) on the Ethereum Consensus Layer. This would ultimately result in no oracle reports and no stETH rebases for this whole period. ::: ## Report processing The [submission](/contracts/accounting-oracle#submitreportdata) of the main report to `AccountingOracle` triggers the next processes in order, although within a single tx: 1. `Accounting._sanityChecks` (via `OracleReportSanityChecker`). 2. `StakingRouter.updateExitedValidatorsCountByStakingModule`. 3. `StakingRouter.reportValidatorBalancesByStakingModule` โ€” stores per-module validator balances used as the basis for rewards distribution. 4. `WithdrawalQueue.onOracleReport` โ€” passes the bunker mode decision. 5. `Accounting.handleOracleReport`, which applies `_applyOracleReportContext` in this exact order: - `Accounting._sanityChecks` (via `OracleReportSanityChecker`) - `IBurner.requestBurnShares(withdrawalQueue, sharesToFinalizeWQ)` (if `sharesToFinalizeWQ > 0`) - `Lido.processClStateUpdate` - `VaultHub.decreaseInternalizedBadDebt` and `Lido.internalizeExternalBadDebt` (if `badDebtToInternalize > 0`) - `IBurner.commitSharesToBurn` (if `totalSharesToBurn > 0`) - `Lido.collectRewardsAndProcessWithdrawals` (finalizes withdrawal queue requests and updates vault transfers) - `Lido.mintShares` (if `sharesToMintAsFees > 0`) - `Accounting._distributeFee` (if `sharesToMintAsFees > 0`) - `StakingRouter.reportRewardsMinted` (if `sharesToMintAsFees > 0`) - `Accounting._notifyRebaseObserver` (emits `TokenRebased` on Lido) 6. `LazyOracle.updateReportData` (stVaults data root). 7. Store extra data (if present). The diagram shows the interaction with contracts. ```mermaid graph TD; A[/submitReportData/] --> B[AccountingOracle]; B --> C[OracleReportSanityChecker]; B --> D[StakingRouter.updateExitedValidatorsCountByStakingModule]; B --> R[StakingRouter.reportValidatorBalancesByStakingModule]; B --> W[WithdrawalQueue.onOracleReport]; B --> E[Accounting.handleOracleReport]; E --> F[Burner.requestBurnShares]; E --> G[Lido.processClStateUpdate]; E --> H[VaultHub.decreaseInternalizedBadDebt]; E --> I[Lido.internalizeExternalBadDebt]; E --> J[Burner.commitSharesToBurn]; E --> K[Lido.collectRewardsAndProcessWithdrawals]; E --> L[Lido.mintShares]; E --> M[Accounting._distributeFee]; E --> N[StakingRouter.reportRewardsMinted]; E --> O["Accounting._notifyRebaseObserver (TokenRebased)"]; B --> P[LazyOracle.updateReportData]; B --> Q[ExtraData]; ``` ## Report data The function `submitReportData()` accepts the following `ReportData` structure. ```solidity struct ReportData { uint256 consensusVersion; uint256 refSlot; uint256 clValidatorsBalanceGwei; uint256 clPendingBalanceGwei; uint256[] stakingModuleIdsWithNewlyExitedValidators; uint256[] numExitedValidatorsByStakingModule; uint256[] stakingModuleIdsWithUpdatedBalance; uint256[] validatorBalancesGweiByStakingModule; uint256 withdrawalVaultBalance; uint256 elRewardsVaultBalance; uint256 sharesRequestedToBurn; uint256[] withdrawalFinalizationBatches; uint256 simulatedShareRate; bool isBunkerMode; bytes32 vaultsDataTreeRoot; string vaultsDataTreeCid; uint256 extraDataFormat; bytes32 extraDataHash; uint256 extraDataItemsCount; } ``` **Oracle consensus info** - `consensusVersion` โ€” Version of the oracle consensus rules. A current version expected by the oracle can be obtained by calling `getConsensusVersion()`. - `refSlot` โ€” Reference slot for which the report was calculated. The state being reported must include all state changes resulting from the all blocks up to this reference slot (inclusive). The epoch containing the slot must be finalized prior to calculating the report. **CL values** - `clValidatorsBalanceGwei` โ€” Sum of balances (`validator.balance`) of all Lido validators on the Ethereum Consensus Layer, excluding pending deposits, nominated in gwei, as observed at the reference slot. - `clPendingBalanceGwei` โ€” Balance of Lido-attributed deposits pending in the Ethereum Consensus Layer deposit queue, nominated in gwei, as observed at the reference slot. - `stakingModuleIdsWithNewlyExitedValidators` โ€” Ids of staking modules that have more exited validators than the number stored in the respective staking module contract as observed at the reference slot. - `numExitedValidatorsByStakingModule` โ€” Number of ever exited validators for each of the staking modules from the `stakingModuleIdsWithNewlyExitedValidators` array as observed at the reference slot. - `stakingModuleIdsWithUpdatedBalance` โ€” Ids of staking modules with updated validator balances as observed at the reference slot. Must include all registered staking modules in their registration order. - `validatorBalancesGweiByStakingModule` โ€” Sum of validator balances, excluding pending deposits, nominated in gwei, for each staking module from the `stakingModuleIdsWithUpdatedBalance` array as observed at the reference slot. **EL values** - `withdrawalVaultBalance` โ€” Ether balance of the Lido [withdrawal vault](/contracts/withdrawal-vault) as observed at the reference slot. - `elRewardsVaultBalance` โ€” Ether balance of the Lido [execution layer rewards vault](/contracts/lido-execution-layer-rewards-vault) as observed at the reference slot. - `sharesRequestedToBurn` โ€” The shares amount requested to burn through [Burner](/contracts/burner) as observed at the reference slot. The value can be obtained in the following way: ```solidity (coverSharesToBurn, nonCoverSharesToBurn) = IBurner(burner).getSharesRequestedToBurn() sharesRequestedToBurn = coverSharesToBurn + nonCoverSharesToBurn ``` **Withdrawals finalization decision** - `withdrawalFinalizationBatches` โ€” The ascendingly-sorted array of withdrawal request IDs obtained by the oracle daemon on report gathering via calling [`WithdrawalQueue.calculateFinalizationBatches`](/contracts/withdrawal-queue-erc721#calculatefinalizationbatches). An empty array means that no withdrawal requests to be finalized. - `simulatedShareRate` โ€” The share rate (i.e., [total pooled ether](/contracts/lido#gettotalpooledether) divided by [total shares](/contracts/lido#gettotalshares)) with the 10^27 precision (i.e., multiplied by 10^27) that would be effective as the result of applying this oracle report at the reference slot, with `withdrawalFinalizationBatches` set to empty array and `simulatedShareRate` set to 0. To estimate `simulatedShareRate` use the view method `Accounting.simulateOracleReport` and calculate as follows: ```solidity _simulatedShareRate = (postTotalPooledEther * 10**27) / postTotalShares ``` where `postTotalPooledEther` and `postTotalShares` were retrieved as return values from the performed view call - `isBunkerMode` โ€” Whether, based on the state observed at the reference slot, the protocol must be in the bunker mode or the turbo (regular) mode. **Staking Vaults** - `vaultsDataTreeRoot` โ€” Merkle Tree root of the stVaults data. - `vaultsDataTreeCid` โ€” CID of the published Merkle tree of the vault data. :::note ##### Extra data Extra data โ€” the oracle information that allows asynchronous processing, potentially in chunks, after the main data is processed. The oracle doesn't enforce that extra data attached to the same data report is processed in full before the processing deadline expires or a new data report starts being processed, but enforces that no processing of extra data for a report is possible after its processing deadline passes or a new data report arrives. Depending on the size of the extra data, the processing might need to be split into multiple transactions. Each transaction contains a chunk of report data (an array of items) and the hash of the next transaction. The last transaction will contain ZERO_HASH as the next transaction hash. 32 bytes array of items | nextHash | ... Extra data is an array of items, each item being encoded as follows: 3 bytes 2 bytes X bytes | itemIndex | itemType | itemPayload | - `itemIndex` is a 0-based index into the extra data array; - `itemType` is the type of extra data item; - `itemPayload` is the item's data which interpretation depends on the item's type. Items must be sorted ascendingly by the `(itemType, ...itemSortingKey)` compound key where `itemSortingKey` calculation depends on the item's type (see below). --- **`itemType=2`** (`EXTRA_DATA_TYPE_EXITED_VALIDATORS`): exited validators by node operators. The `itemPayload` field has the following format: | 3 bytes | 8 bytes | nodeOpsCount * 8 bytes | nodeOpsCount * 16 bytes | | moduleId | nodeOpsCount | nodeOperatorIds | exitedValidatorsCounts | `moduleId` is the staking module for which exited keys counts are being reported. `nodeOperatorIds` contains an array of ids of node operators that have total exited validators counts changed compared to the staking module smart contract storage as observed at the reference slot. Each id is a 8-byte uint, ids are packed tightly. `nodeOpsCount` contains the number of node operator ids contained in the nodeOperatorIds array. Thus, nodeOpsCount = byteLength(nodeOperatorIds) / 8 `exitedValidatorsCounts` contains an array of exited validators total counts, as observed at the reference slot, for the node operators from the nodeOperatorIds array, in the same order. Each count is a 16-byte uint, counts are packed tightly. Thus, byteLength(exitedValidatorsCounts) = nodeOpsCount * 16 `nodeOpsCount` must not be greater than `maxNodeOperatorsPerExtraDataItem` specified in the [`OracleReportSanityChecker`](./oracle-report-sanity-checker) contract. If a staking module has more node operators with total exited validators counts changed compared to the staking module smart contract storage (as observed at the reference slot), reporting for that module should be split into multiple items. Item sorting key is a compound key consisting of the module id and the first reported node operator's id: itemSortingKey = (moduleId, nodeOperatorIds[0:8]) --- **Deprecated: `itemType=1`** (`EXTRA_DATA_TYPE_STUCK_VALIDATORS`): This type was deprecated in the Triggerable Withdrawals update. The mechanism for handling stuck validator keys is no longer supported. Submitting this type will revert with `DeprecatedExtraDataType`. --- The oracle daemon must report exited validators counts ONLY for those `(moduleId, nodeOperatorId)` pairs that contain outdated counts in the staking module smart contract as observed at the reference slot. Extra data array can be passed in different formats, see below. ::: - `extraDataFormat` - Format of the extra data. Currently, only the `EXTRA_DATA_FORMAT_EMPTY=0` and `EXTRA_DATA_FORMAT_LIST=1` formats are supported. See the constant defining a specific data format for more info. - `extraDataHash` - Hash of the extra data. See the constant defining a specific extra data format for the info on how to calculate the hash. Must be set to a zero hash if the oracle report contains no extra data. - `extraDataItemsCount` - Number of the extra data items. Must be set to zero if the oracle report contains no extra data. ## Access and permissions Access to lever methods is restricted using the functionality of the [AccessControlEnumerable](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/utils/access/AccessControlEnumerable.sol) contract and a bunch of [granular roles](#permissions). ## Constants ### LOCATOR() Returns an address of the [LidoLocator](/contracts/lido-locator) contract ```solidity ILidoLocator public immutable LOCATOR; ``` ### SECONDS_PER_SLOT() See [https://ethereum.org/en/developers/docs/blocks/#block-time](https://ethereum.org/en/developers/docs/blocks/#block-time) :::note always returns 12 seconds due to [the Merge](https://ethereum.org/en/roadmap/merge/) ::: ```solidity uint256 public immutable SECONDS_PER_SLOT; ``` ### GENESIS_TIME() See [https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21](https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21) :::note always returns 1606824023 (December 1, 2020, 12:00:23pm UTC) on [Mainnet](https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21) ::: ```solidity uint256 public immutable GENESIS_TIME; ``` ### EXTRA_DATA_TYPE_STUCK_VALIDATORS() **Deprecated.** This type was previously used for stuck validators but is no longer supported. Submitting this type will revert. ```solidity uint256 public constant EXTRA_DATA_TYPE_STUCK_VALIDATORS = 1; ``` ### EXTRA_DATA_TYPE_EXITED_VALIDATORS() This type contains the details of [exited](/contracts/staking-router#exited-validators) validator(s). ```solidity uint256 public constant EXTRA_DATA_TYPE_EXITED_VALIDATORS = 2; ``` ### EXTRA_DATA_FORMAT_EMPTY() The extra data format used to signify that the oracle report contains no [extra data](/contracts/accounting-oracle#extra-data). Sends as a part of the Oracle's [Phase 3](/guides/oracle-operator-manual#phase-3-submitting-a-report-extra-data). This format uses when there are no new [exited](/contracts/staking-router#exited-validators) validators on report period. ```solidity uint256 public constant EXTRA_DATA_FORMAT_EMPTY = 0; ``` ### EXTRA_DATA_FORMAT_LIST() The list format for the extra data array. Used when the oracle report contains extra data. Extra data may be split across one or more transactions. Each transaction contains a 32-byte `keccak256` hash of the next transaction's data (or a zero hash if there is none), followed by a chunk of report items: ```txt | 32 bytes | X bytes | | Next transaction's data hash or `ZERO_HASH` | array of items | ``` The `extraDataHash` in `ReportData` is the hash of the first transaction's data, with each chunk's hash committing to the next one: ```txt extraDataHash := hash0 hash0 := keccak256(| hash1 | extraData[0], ... extraData[n] |) hash1 := keccak256(| hash2 | extraData[n + 1], ... extraData[m] |) ... hashK := keccak256(| ZERO_HASH | extraData[x + 1], ... extraData[extraDataItemsCount] |) ``` ```solidity uint256 public constant EXTRA_DATA_FORMAT_LIST = 1; ``` ## ProcessingState ```solidity struct ProcessingState { uint256 currentFrameRefSlot; uint256 processingDeadlineTime; bytes32 mainDataHash; bool mainDataSubmitted; bytes32 extraDataHash; uint256 extraDataFormat; bool extraDataSubmitted; uint256 extraDataItemsCount; uint256 extraDataItemsSubmitted; } ``` - `currentFrameRefSlot` - Reference slot for the current reporting frame. - `processingDeadlineTime` - The last time at which a data can be submitted for the current reporting frame. - `mainDataHash` - Hash of the main report data. Zero bytes if consensus on the hash hasn't been reached yet for the current reporting frame. - `mainDataSubmitted` - Whether the main report data for the current reporting frame has already been submitted. - `extraDataHash` - Hash of the extra report data. Should be ignored unless `mainDataSubmitted` is true. - `extraDataFormat` - Format of the extra report data for the current reporting frame. Should be ignored unless `mainDataSubmitted` is true. - `extraDataSubmitted` - Whether any extra report data for the current reporting frame has been submitted. - `extraDataItemsCount` - Total number of extra report data items for the current reporting frame. Should be ignored unless `mainDataSubmitted` is true. - `extraDataItemsSubmitted` - How many extra report data items are already submitted for the current reporting frame. ## View methods ### getConsensusContract() Returns the address of the [HashConsensus](/contracts/hash-consensus) contract instance used by `AccountingOracle`. ```solidity function getConsensusContract() external view returns (address); ``` ### getConsensusReport() Returns the last consensus report hash and metadata. ```solidity function getConsensusReport() external view returns ( bytes32 hash, uint256 refSlot, uint256 processingDeadlineTime, bool processingStarted ); ``` #### Returns | Name | Type | Description | | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `hash` | `bytes32` | The last reported hash | | `refSlot` | `uint256` | The frame's reference slot: if the data the consensus is being reached upon includes or depends on any onchain state, this state should be queried at the reference slot. The state being reported must include all state changes resulting from all blocks up to this reference slot (inclusive). | | `processingDeadlineTime` | `uint256` | Timestamp of the last slot at which a report can be reported and processed | | `processingStarted` | `bool` | Has the processing of the report been started or not | ### getConsensusVersion() Returns the current consensus version expected by the oracle contract. :::note Consensus version must change every time consensus rules change, meaning that an oracle looking at the same reference slot would calculate a different hash. ::: ```solidity function getConsensusVersion() external view returns (uint256); ``` ### getContractVersion() Returns the current contract version. ```solidity function getContractVersion() public view returns (uint256); ``` ### getCurrentFrame() Returns the reference slot of the current reporting frame and its timestamp. ```solidity function getCurrentFrame() external view returns (uint256 refSlot, uint256 refSlotTimestamp); ``` ### getLastProcessingRefSlot() Returns the last reference slot for which processing of the report was started. ```solidity function getLastProcessingRefSlot() external view returns (uint256); ``` ### getProcessingState() Returns data processing state for the current reporting frame. See the docs for the [ProcessingState](#processingstate) struct. ```solidity function getProcessingState() external view returns (ProcessingState memory result); ``` ## Methods ### submitReportData() Submits report data for processing. ```solidity function submitReportData(ReportData calldata data, uint256 contractVersion); ``` #### Parameters | Name | Type | Description | | ----------------- | ------------ | ------------------------------------------------------------------------------------------------------ | | `data` | `ReportData` | The data. See the [ReportData](/contracts/accounting-oracle#report-data) structure's docs for details. | | `contractVersion` | `uint256` | Expected version of the oracle contract. | #### Reverts For more information about reverts, see a separate section [here](#reverts-3) ### submitReportExtraDataEmpty() Triggers the processing required when no extra data is present in the report, i.e. when extra data format equals EXTRA_DATA_FORMAT_EMPTY. ```solidity function submitReportExtraDataEmpty(); ``` #### Reverts - Reverts with `SenderNotAllowed()` if sender doesn't have a `SUBMIT_DATA_ROLE` role and sender is not a consensus member. ### submitReportExtraDataList() Submits report extra data in the EXTRA_DATA_FORMAT_LIST format for processing. ```solidity function submitReportExtraDataList(bytes calldata items) ``` #### Parameters | Name | Type | Description | | ------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | `items` | `bytes` | The extra data items list. See docs for the [EXTRA_DATA_FORMAT_LIST](#extra_data_format_list) constant for details. | #### Reverts - Reverts with `SenderNotAllowed()` if sender doesn't have a `SUBMIT_DATA_ROLE` role and sender is not a consensus member. ### submitConsensusReport() Called by [AccountingOracle HashConsensus](/contracts/hash-consensus) contract to push a consensus report for processing. :::note Note that submitting the report doesn't require the processor to start processing it right away, this can happen later (see [`getLastProcessingRefSlot`](#getlastprocessingrefslot)). Until processing is started, HashConsensus is free to reach consensus on another report for the same reporting frame an submit it using this same function, or to lose the consensus on the submitted report, notifying the processor via `discardConsensusReport`. ::: ```solidity function submitConsensusReport(bytes32 reportHash, uint256 refSlot, uint256 deadline) ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ----------------------------------------------------------------------------------------------------- | | `reportHash` | `bytes32` | Hash of the data calculated for the given reference slot. | | `refSlot` | `uint256` | The reference slot the data was calculated for. Reverts if doesn't match the current reference slot. | | `deadline` | `uint256` | The timestamp of the last slot at which the report can be processed by the report processor contract. | ### discardConsensusReport() Called by HashConsensus contract to notify that the report for the given ref. slot is not a consensus report anymore and should be discarded. This can happen when a member changes their report, is removed from the set, or when the quorum value gets increased. Only called when, for the given reference slot: 1. there previously was a consensus report; AND 2. processing of the consensus report hasn't started yet; AND 3. report processing deadline is not expired yet; AND 4. there's no consensus report now (otherwise, [submitConsensusReport](#submitconsensusreport) is called instead). Can be called even when there's no submitted non-discarded consensus report for the current reference slot, i.e. can be called multiple times in succession. ```solidity function discardConsensusReport(uint256 refSlot) ``` ### setConsensusContract() ```solidity function setConsensusContract(address addr) ``` ### setConsensusVersion() Sets the consensus version expected by the oracle contract. ```solidity function setConsensusVersion(uint256 version) ``` ## Permissions ### SUBMIT_DATA_ROLE() An ACL role granting the permission to submit the data for a committee report. ```solidity bytes32 public constant SUBMIT_DATA_ROLE = keccak256("SUBMIT_DATA_ROLE"); ``` ### MANAGE_CONSENSUS_CONTRACT_ROLE() An ACL role granting the permission to set the consensus contract address by calling setConsensusContract. ```solidity bytes32 public constant MANAGE_CONSENSUS_CONTRACT_ROLE = keccak256("MANAGE_CONSENSUS_CONTRACT_ROLE"); ``` ### MANAGE_CONSENSUS_VERSION_ROLE() An ACL role granting the permission to set the consensus version by calling setConsensusVersion. ```solidity bytes32 public constant MANAGE_CONSENSUS_VERSION_ROLE = keccak256("MANAGE_CONSENSUS_VERSION_ROLE"); ``` ## Events ### ExtraDataSubmitted() Emits when any extra report data for the current reporting frame has been submitted. ```solidity ExtraDataSubmitted(uint256 indexed refSlot, uint256 itemsProcessed, uint256 itemsCount) ``` ### WarnExtraDataIncompleteProcessing() Emits when try to submit the same report, but not all items are processed yet. ```solidity event WarnExtraDataIncompleteProcessing( uint256 indexed refSlot, uint256 processedItemsCount, uint256 itemsCount ) ``` ### ConsensusHashContractSet() Emits when a contract hash value is changed. ```solidity event ConsensusHashContractSet(address indexed addr, address indexed prevAddr) ``` ### ConsensusVersionSet() Emits when a consensus version value is changed. ```solidity event ConsensusVersionSet(uint256 indexed version, uint256 indexed prevVersion) ``` ### ReportSubmitted() Emits when a new consensus report hash is submitted ```solidity event ReportSubmitted(uint256 indexed refSlot, bytes32 hash, uint256 processingDeadlineTime) ``` ### ReportDiscarded() Emits when consensus report is discarded. ```solidity event ReportDiscarded(uint256 indexed refSlot, bytes32 hash) ``` ### ProcessingStarted() Emits when report data is submitted ```solidity event ProcessingStarted(uint256 indexed refSlot, bytes32 hash) ``` ### WarnProcessingMissed() Emits on [submitConsensusReport](#submitconsensusreport) when `refSlot != prevSubmittedRefSlot && prevProcessingRefSlot != prevSubmittedRefSlot` ```solidity event WarnProcessingMissed(uint256 indexed refSlot) ``` ## Reverts ### submitReportData() To ensure that the reported data is within possible values, the handler function performs a number of sanity checks. When checking, reverts may occur in different contracts. #### AccountingOracle and BaseOracle contracts - Reverts with `SenderNotAllowed()` if caller doesn't have a `SUBMIT_DATA_ROLE` role and is not a member of the oracle committee. - Reverts with `UnexpectedContractVersion(expectedVersion, version)` if provided contract version is different from the current one. - Reverts with `UnexpectedConsensusVersion(expectedConsensusVersion, consensusVersion)` if provided consensus version is different from the expected one. - Reverts with `UnexpectedRefSlot(report.refSlot, refSlot)` if provided reference slot differs from the current consensus frame's one. - Reverts with `UnexpectedDataHash(report.hash, hash)` if keccak256 hash of the ABI-encoded data is different from the last hash. - Reverts with `NoConsensusReportToProcess()` if report hash data is 0. - Reverts with `ProcessingDeadlineMissed(uint256 deadline)` if the processing deadline for the current consensus frame is missed. - Reverts with `RefSlotAlreadyProcessing()` if report reference slot is equal to previous processing reference slot. - Reverts with `UnexpectedExtraDataHash(bytes32(0), data.extraDataHash)` if `data.extraDataFormat` is `EXTRA_DATA_FORMAT_EMPTY` and `data.extraDataHash` is not 0 - Reverts with `UnexpectedExtraDataItemsCount(0, data.extraDataItemsCount)` if `data.extraDataFormat` is `EXTRA_DATA_FORMAT_EMPTY` and `data.extraDataItemsCount` is not 0 - Reverts with `UnsupportedExtraDataFormat(data.extraDataFormat)` if `data.extraDataFormat` is not `EXTRA_DATA_FORMAT_EMPTY` and not `EXTRA_DATA_FORMAT_LIST` - Reverts with `ExtraDataItemsCountCannotBeZeroForNonEmptyData()` if `data.extraDataFormat` is `EXTRA_DATA_FORMAT_LIST` and `data.extraDataItemsCount` is 0 - Reverts with `ExtraDataHashCannotBeZeroForNonEmptyData()` if `data.extraDataFormat` is `EXTRA_DATA_FORMAT_LIST` and `data.extraDataHash` is 0 - Reverts with `InvalidExitedValidatorsData()` if provided exited validators data doesn't meet safety checks. - Reverts with `DeprecatedExtraDataType(itemIndex, itemType)` on `submitReportExtraDataList()` if extra data contains the deprecated `EXTRA_DATA_TYPE_STUCK_VALIDATORS` type. #### OracleReportSanityChecker - Reverts with `TooManyItemsPerExtraDataTransaction(uint256 maxItemsCount, uint256 receivedItemsCount)` error when check is failed, more [here](/contracts/oracle-report-sanity-checker#checkextradataitemscountpertransaction) - Reverts with `ExitedEthAmountPerDayLimitExceeded(uint256 limitPerDay, uint256 exitedPerDay)` if provided exited validators data doesn't meet safety checks. - Reverts with `InvalidClBalancesData()` if the per-module validator balances arrays have inconsistent lengths. - Reverts with `InconsistentValidatorsBalanceByModule(uint256 expected, uint256 actual)` if the sum of per-module validator balances doesn't equal the reported total CL validators balance. - Reverts with `IncorrectTotalPendingBalance(uint256 maxAllowed, uint256 actual)`, `IncorrectTotalActivatedBalance(uint256 maxAllowed, uint256 actual)`, `IncorrectTotalCLBalanceIncrease(uint256 maxAllowed, uint256 actual)`, or `IncorrectTotalModuleValidatorsBalanceIncrease(uint256 maxAllowed, uint256 actual)` if the reported balance changes exceed the configured daily limits. #### StakingRouter - Reverts with `ArraysLengthMismatch()` if the lengths of the provided module ids and values arrays don't match, or if the balances report doesn't cover all registered staking modules. - Reverts with `UnexpectedModuleId(uint256 expected, uint256 received)` if the balances report lists staking modules out of their registration order. - Reverts with `ExitedValidatorsCountCannotDecrease()` if provided exited validators data doesn't meet safety checks. - Reverts with `ReportedExitedValidatorsExceedDeposited(uint256 reportedExitedValidatorsCount, uint256 depositedValidatorsCount)` if provided exited validators data doesn't meet safety checks. Other reverts on `Accounting.handleOracleReport()` --- # Burner - [Source Code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/Burner.sol) - [Deployed Contract](https://etherscan.io/address/0xE76c52750019b80B43E36DF30bf4060EB73F573a) The contract provides a way for Lido protocol to burn stETH token shares as a means to finalize withdrawals, penalize untimely exiting node operators, and, possibly, cover losses in staking. It relies on the [rebasing](/contracts/lido#rebase) nature of stETH. The `Lido` contract calculates user balance using the following equation: `balanceOf(account) = shares[account] * totalPooledEther / totalShares`. Therefore, burning shares (e.g. decreasing the `totalShares` amount) increases stETH holders' balances. It's presumed that actual shares burning happens inside the [`Lido`](/contracts/lido) contract as a part of the [`AccountingOracle`](/contracts/accounting-oracle) report. `Burner` provides a safe and deterministic way to incur a positive stETH token rebase by gradually decreasing `totalShares` that can be correctly handled by 3rd party protocols integrated with stETH. `Burner` accepts burning requests in the following two ways: - Locking **someone's pre-approved** stETH by the caller with the assigned `REQUEST_BURN_SHARES_ROLE`; - Locking **caller-provided** stETH with the `REQUEST_BURN_MY_STETH_ROLE` assigned role. Those burn requests are initially set by the contract to a pending state. Actual burning happens as part of an oracle ([`AccountingOracle`](/contracts/accounting-oracle)) report handling by [`Accounting`](/contracts/accounting) to prevent additional fluctuations of the existing stETH token rebase period (~24h). We also distinguish two types of shares burn requests: - request to **cover** a slashing event (e.g. decreasing of the total pooled ETH amount between the two consecutive oracle reports); - request to burn shares for any other cases (**non-cover**). The contract has two separate counters for the burnt shares: cover and non-cover ones. The contract is exclusively responsible for the stETH shares burning by [`Lido`](/contracts/lido) and burning allowed only from the contract's own balance only. ## Shares burnt counters The contract keeps count of all shares ever burned by way of maintaining two internal counters: `totalCoverSharesBurnt` and `totalNonCoverSharesBurnt` for cover and non-cover burns, respectively. These counters are increased when actual stETH burn is performed as part of the Lido Oracle report. This makes it possible to split any stETH rebase into two sub-components: the rewards-induced rebase and cover application-induced rebase, which can be done as follows: 1. Before the rebase, store the previous values of both counters, as well as the value of stETH share price: ```sol prevCoverSharesBurnt = Burner.totalCoverSharesBurnt() prevSharePrice = stETH.totalSupply() / stETH.getTotalShares() ``` 2. After the rebase, perform the following calculations: ```sol sharesBurntFromOldToNew = Burner.totalCoverSharesBurnt() - prevCoverSharesBurnt; newSharePriceAfterCov = stETH.totalSupply() / (stETH.getTotalShares() + sharesBurntFromOldToNew); newSharePrice = stETH.totalSupply() / stETH.getTotalShares(); // rewards-induced share price increase rewardPerShare = newSharePriceAfterCov - prevSharePrice; // cover-induced share price increase nonRewardSharePriceIncrease = newSharePrice - prevSharePrice - rewardPerShare; ``` ## Constants ### LOCATOR() Returns the address of the [LidoLocator](/contracts/lido-locator) contract. ```sol ILidoLocator public immutable LOCATOR; ``` ### LIDO() Returns the address of the [Lido](/contracts/lido) (stETH) contract. ```sol ILido public immutable LIDO; ``` ## Roles ### REQUEST_BURN_MY_STETH_ROLE() An ACL role granting the permission to burn caller's own stETH. ```sol bytes32 public constant REQUEST_BURN_MY_STETH_ROLE = keccak256("REQUEST_BURN_MY_STETH_ROLE"); ``` ### REQUEST_BURN_SHARES_ROLE() An ACL role granting the permission to burn stETH shares on behalf of others. ```sol bytes32 public constant REQUEST_BURN_SHARES_ROLE = keccak256("REQUEST_BURN_SHARES_ROLE"); ``` ## View methods ### getCoverSharesBurnt() Returns the total cover shares ever burnt. ```sol function getCoverSharesBurnt() external view returns (uint256) ``` ### getNonCoverSharesBurnt() Returns the total non-cover shares ever burnt. ```sol function getNonCoverSharesBurnt() external view returns (uint256) ``` ### getExcessStETH() Returns the stETH amount belonging to the burner contract address but not marked for burning. ```sol function getExcessStETH() external view returns (uint256) ``` ### getSharesRequestedToBurn() Returns numbers of cover and non-cover shares requested to burn. ```sol function getSharesRequestedToBurn() external view returns (uint256 coverShares, uint256 nonCoverShares) ``` ### isMigrationAllowed() Returns whether migration from the old Burner is allowed. Used during V3 upgrade only. ```sol function isMigrationAllowed() external view returns (bool) ``` ## Methods ### requestBurnMyStETHForCover() Transfers stETH tokens from the message sender and irreversibly locks these on the burner contract address. Internally converts tokens amount into underlying shares amount and marks the converted shares amount for cover-backed burning by increasing the internal `coverSharesBurnRequested` counter. ```sol function requestBurnMyStETHForCover(uint256 _stETHAmountToBurn) external ``` :::note Reverts if any of the following is true: - `msg.sender` is not a holder of the `REQUEST_BURN_MY_STETH_ROLE` role; - no stETH provided (`_stETHAmountToBurn == 0`); - no stETH transferred (allowance exceeded). ::: #### Parameters | Name | Type | Description | | -------------------- | --------- | ----------------------------------------------- | | `_stETHAmountToBurn` | `uint256` | stETH tokens amount (not shares amount) to burn | ### requestBurnSharesForCover() Transfers stETH shares from `_from` and irreversibly locks these on the burner contract address. Internally marks the shares amount for cover-backed burning by increasing the internal `coverSharesBurnRequested` counter. Can be called only by a holder of `REQUEST_BURN_SHARES_ROLE`. After Lido V2 upgrade not actually called by any contract and supposed to be called by Lido DAO Agent in case of a need for cover. ```sol function requestBurnSharesForCover(address _from, uint256 _sharesAmountToBurn) ``` :::note Reverts if any of the following is true: - `msg.sender` is not a holder of the `REQUEST_BURN_SHARES_ROLE` role; - no stETH shares provided (`_sharesAmountToBurn == 0`); - no stETH shares transferred (allowance exceeded). ::: #### Parameters | Name | Type | Description | | --------------------- | --------- | ----------------------------------------------- | | `_from` | `address` | address to transfer shares from | | `_sharesAmountToBurn` | `uint256` | shares amount (not stETH tokens amount) to burn | ### requestBurnMyShares() Transfers stETH shares from the message sender and irreversibly locks these on the burner contract address. Marks the shares amount for non-cover backed burning by increasing the internal `nonCoverSharesBurnRequested` counter. This is the **preferred method** for burning non-cover shares as it prevents dust accumulation. ```sol function requestBurnMyShares(uint256 _sharesAmountToBurn) external ``` :::note Reverts if any of the following is true: - `msg.sender` is not a holder of the `REQUEST_BURN_MY_STETH_ROLE` role; - no stETH shares provided (`_sharesAmountToBurn == 0`); - no stETH shares transferred (allowance exceeded). ::: #### Parameters | Name | Type | Description | | --------------------- | --------- | ----------------------------------------------- | | `_sharesAmountToBurn` | `uint256` | shares amount (not stETH tokens amount) to burn | ### requestBurnMyStETH() :::warning **DEPRECATED**: Use `requestBurnMyShares` instead to prevent dust accumulation. ::: Transfers stETH tokens from the message sender and irreversibly locks these on the burner contract address. Internally converts tokens amount into underlying shares amount and marks the converted amount for non-cover backed burning by increasing the internal `nonCoverSharesBurnRequested` counter. ```sol function requestBurnMyStETH(uint256 _stETHAmountToBurn) external ``` :::note Reverts if any of the following is true: - `msg.sender` is not a holder of the `REQUEST_BURN_MY_STETH_ROLE` role; - no stETH provided (`_stETHAmountToBurn == 0`); - no stETH transferred (allowance exceeded). ::: #### Parameters | Name | Type | Description | | -------------------- | --------- | ----------------------------------------------- | | `_stETHAmountToBurn` | `uint256` | stETH tokens amount (not shares amount) to burn | ### requestBurnShares() Transfers stETH shares from `_from` and irreversibly locks these on the burner contract address. Internally marks the shares amount for non-cover backed burning by increasing the internal `nonCoverSharesBurnRequested` counter. Can be called only by a holder of the `REQUEST_BURN_SHARES_ROLE` role which after Lido V2 upgrade is either [`Lido`](/contracts/lido) or [`NodeOperatorsRegistry`](/contracts/node-operators-registry). [`Lido`](/contracts/lido) needs this to request shares locked on the [`WithdrawalQueueERC721`](/contracts/withdrawal-queue-erc721) and [`NodeOperatorsRegistry`](/contracts/node-operators-registry) needs it to request burning shares to penalize the rewards of misbehaving node operators. ```sol function requestBurnShares(address _from, uint256 _sharesAmountToBurn) ``` :::note Reverts if any of the following is true: - `msg.sender` is not a holder of `REQUEST_BURN_SHARES_ROLE` role; - no stETH shares provided (`_sharesAmountToBurn == 0`); - no stETH shares transferred (allowance exceeded). ::: #### Parameters | Name | Type | Description | | --------------------- | --------- | ----------------------------------------------- | | `_from` | `address` | address to transfer shares from | | `_sharesAmountToBurn` | `uint256` | shares amount (not stETH tokens amount) to burn | ### recoverExcessStETH() Transfers the excess stETH amount (e.g. belonging to the burner contract address but not marked for burning) to the Lido treasury address (the `DAO Agent` contract) set upon the contract construction. Does nothing if the `getExcessStETH` view func returns 0 (zero), i.e. there is no excess stETH on the contract's balance. ```sol function recoverExcessStETH() external ``` ### recoverERC20() Transfers a given amount of an ERC20-token (defined by the provided contract address) belonging to the burner contract address to the Lido treasury (the `DAO Agent` contract) address. ```sol function recoverERC20(address _token, uint256 _amount) external ``` :::note Reverts if any of the following is true: - `_amount` value is 0 (zero); - `_token` address is 0 (zero); - `_token` address equals to the `stETH` address (use `recoverExcessStETH` instead). ::: #### Parameters | Name | Type | Description | | --------- | --------- | ----------------------------------------- | | `_token` | `address` | ERC20-compatible token address to recover | | `_amount` | `uint256` | Amount to recover | ### recoverERC721() Transfers a given ERC721-compatible NFT (defined by the contract address) belonging to the burner contract address to the Lido treasury (the `DAO Agent`) address. ```sol function recoverERC721(address _token, uint256 _tokenId) external ``` :::note Reverts if any of the following is true: - `_token` address is 0 (zero); - `_token` address equals to the `stETH` address (use `recoverExcessStETH` instead). ::: #### Parameters | Name | Type | Description | | ---------- | --------- | ------------------------------------------ | | `_token` | `address` | ERC721-compatible token address to recover | | `_tokenId` | `uint256` | Token id to recover | ### commitSharesToBurn() Marks previously requested to burn cover and non-cover share as burnt. Emits `StETHBurnt` event for the cover and non-cover shares marked as burnt. Performs the actual shares burning by calling `LIDO.burnShares()`. This function is called by the [`Accounting`](/contracts/accounting) contract as part of oracle report handling. If `_sharesToBurn` is 0 does nothing. ```sol function commitSharesToBurn(uint256 _sharesToBurn) external ``` :::note Reverts if any of the following is true: - `msg.sender` address is NOT equal to the `Accounting` contract address (via `LOCATOR.accounting()`); - `_sharesToBurn` is greater than the cover plus non-cover shares requested to burn. ::: #### Parameters | Name | Type | Description | | --------------- | --------- | ------------------------------------------------------ | | `_sharesToBurn` | `uint256` | Amount of cover plus non-cover shares to mark as burnt | ### initialize() Initializes the contract by setting up roles and migration allowance. Should be called only once during deployment. ```sol function initialize(address _admin, bool _isMigrationAllowed) external ``` #### Parameters | Name | Type | Description | | --------------------- | --------- | -------------------------------------------- | | `_admin` | `address` | Address to be granted the DEFAULT_ADMIN_ROLE | | `_isMigrationAllowed` | `bool` | Whether migration from old Burner is allowed | ### migrate() Migrates state from the old Burner contract. Can be called only by the Lido contract and only once. ```sol function migrate(address _oldBurner) external ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ---------------------------------- | | `_oldBurner` | `address` | Address of the old Burner contract | ## Events ### StETHBurnRequested Emitted when a new stETH burning request is added. ```sol event StETHBurnRequested( bool indexed isCover, address indexed requestedBy, uint256 amountOfStETH, uint256 amountOfShares ) ``` ### StETHBurnt Emitted when stETH is burnt. ```sol event StETHBurnt(bool indexed isCover, uint256 amountOfStETH, uint256 amountOfShares) ``` ### ExcessStETHRecovered Emitted when excess stETH is recovered to the treasury. ```sol event ExcessStETHRecovered(address indexed requestedBy, uint256 amountOfStETH, uint256 amountOfShares) ``` ### ERC20Recovered Emitted when ERC20 tokens are recovered to the treasury. ```sol event ERC20Recovered(address indexed requestedBy, address indexed token, uint256 amount) ``` ### ERC721Recovered Emitted when ERC721 NFTs are recovered to the treasury. ```sol event ERC721Recovered(address indexed requestedBy, address indexed token, uint256 tokenId) ``` --- # CircuitBreaker An emergency-pause layer for Lido protocol contracts. | Network | Address | |---------|-------------------------------------------------------------------------------------------------------------------------------| | Mainnet | [`0x6019CB557978296BA3C08a7B73225C0975DFB2F7`](https://etherscan.io/address/0x6019CB557978296BA3C08a7B73225C0975DFB2F7) | | Hoodi | [`0x44a5789dFeDa59cD176Ab5709ec2F4829dE4d555`](https://hoodi.etherscan.io/address/0x44a5789dFeDa59cD176Ab5709ec2F4829dE4d555) | ## What is CircuitBreaker? CircuitBreaker is a single, permanent contract that lets DAO-designated pauser committees instantly pause registered Lido contracts for a bounded duration without waiting for a governance vote. It is the successor to GateSeal: instead of single-use, expiring instances that must be redeployed every year, CircuitBreaker operates indefinitely. - [Source code](https://github.com/lidofinance/circuit-breaker/blob/main/src/CircuitBreaker.sol) - [Repository](https://github.com/lidofinance/circuit-breaker) - [LIP-34: Programmable panic layer](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-34.md) - [Research forum proposal](https://research.lido.fi/t/circuitbreaker-programmable-panic-layer/11400/1) ## Why use a CircuitBreaker? Putting critical Lido components on hold via a DAO vote can take many days. CircuitBreaker provides a way to temporarily pause these contracts immediately while the DAO investigates, deliberates, and executes a decision. It is operated by committees, multisig accounts authorized to pull the brake in an emergency. Granting a committee unilateral pause authority is non-trivial, so CircuitBreaker has a number of safeguards: - **Single-use per pausable**: a successful pause unregisters the committee from that pausable contract. To pause the same contract again, the pauser must be re-assigned by a full DAO vote. A misbehaving committee can pause only its assigned contracts and only once. - **Bounded pause duration**: the pause has a limited duration controlled by the DAO, i.e. the pauser does not choose the duration when triggering the pause. - **Pause only**: CircuitBreaker holds only the pause role on its registered pausables. The contract cannot resume the pausables, doesn't manage funds, doesn't have a proxy. - **Liveness via heartbeats**: each pauser maintains its own heartbeat. If the heartbeat expires, the pauser can neither pause nor self-prolong authority. This means that unresponsive committees lose authority automatically. - **Immutable admin**: the admin address is set at construction and cannot be changed, eliminating ownership-transfer exploits. ### Roles - **Admin** โ€” an immutable address, the DAO Agent. Configures the registry and controls the pause duration and heartbeat interval. - **Pauser** โ€” a multisig committee assigned to one or more pausables. Can pause any pausable it is registered for, and must periodically call `heartbeat()` to remain authorized. ### Immutable bounds and current values CircuitBreaker is deployed with immutable bounds: - `MIN_PAUSE_DURATION` / `MAX_PAUSE_DURATION` โ€” inclusive lower/upper bounds for `pauseDuration`. - `MIN_HEARTBEAT_INTERVAL` / `MAX_HEARTBEAT_INTERVAL` โ€” inclusive lower/upper bounds for `heartbeatInterval`. Within these bounds, the admin can adjust `pauseDuration` and `heartbeatInterval` at any time without redeployment. Changes to `heartbeatInterval` apply only to **subsequent** heartbeats, i.e. already-stored expiries are not retroactively updated. The deployed parameter sets are: | Parameter | Mainnet | Hoodi | |----------------------------|----------------------|----------------------| | `MIN_PAUSE_DURATION` | 5 days (432,000 s) | 60 s | | `MAX_PAUSE_DURATION` | 60 days (5,184,000 s)| 30 days (2,592,000 s)| | `MIN_HEARTBEAT_INTERVAL` | 30 days (2,592,000 s)| 60 s | | `MAX_HEARTBEAT_INTERVAL` | 3 years (94,608,000 s)| 3 years (94,608,000 s)| | Initial `pauseDuration` | 21 days (1,814,400 s)| 1 hour (3,600 s) | | Initial `heartbeatInterval`| 1 year (31,536,000 s)| 1 year (31,536,000 s)| The mainnet 21-day initial pause duration is sized to cover the worst-case governance timeline: two consecutive Aragon votes (โ‰ˆ10 days), a minimum Dual Governance timelock (4 days), and a 7-day buffer for analysis and coordination. Hoodi uses relaxed bounds appropriate for testnet drills. ### Heartbeat mechanism Each pauser has its own heartbeat expiry timestamp. The pauser is considered *live* while their expiry timestamp is in the future. While live, the pauser can pause any of its assigned contracts and can extend its expiry by sending a heartbeat. Once the expiry passes, the pauser is no longer considered live and can neither pause nor extend expiry. A heartbeat is a drill transaction that updates the caller's heartbeat. An expired pauser cannot revive itself, so the pauser must renew their heartbeat before it expires. The expiry is also updated on registration and pause: - When the DAO assigns a pauser to a pausable, that pauser's expiry is updated, regardless of its previous value. - When a pauser is unassigned from its last remaining pausable (either by the DAO or by triggering a pause) its expiry is cleared. - A successful pause that leaves the caller with at least one other assigned pausable refreshes the caller's expiry the same way a heartbeat would. ### Pause flow When a registered, live pauser triggers a pause on one of its assigned pausables, CircuitBreaker: 1. Unregisters the pauser from that pausable. 2. Pauses the pausable for the preconfigured pause duration. The pausable is expected to follow the [`PausableUntil`](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/utils/PausableUntil.sol) pattern. 3. Reads back the pausable's state to confirm the pause actually took effect, reverting if it did not. 4. Updates the caller's heartbeat expiry as described above. A reentrancy guard prevents a malicious pausable from calling back into CircuitBreaker during this flow to trigger additional pauses. CircuitBreaker does not verify at registration time that a pausable implements the expected interface or that CircuitBreaker has been granted the pause role on it. These properties can also change later, for example through a proxy upgrade or a role revocation. The DAO is therefore responsible for ensuring the pause role is granted before assigning a pauser. ## Covered pausables The set of pausables and their assigned pausers is maintained by the DAO. See the deployed-contracts pages for the current registry on each network: - [Mainnet deployments โ€” CircuitBreaker](/deployed-contracts/#circuit-breaker) - [Hoodi deployments โ€” CircuitBreaker](/deployed-contracts/hoodi#circuit-breaker) ## View Methods ### ADMIN() Returns the immutable admin address. ```solidity function ADMIN() external view returns (address); ``` ### MIN_PAUSE_DURATION() / MAX_PAUSE_DURATION() Inclusive lower and upper bounds, in seconds, for `pauseDuration`. Set at deployment, immutable thereafter. ```solidity function MIN_PAUSE_DURATION() external view returns (uint256); function MAX_PAUSE_DURATION() external view returns (uint256); ``` ### MIN_HEARTBEAT_INTERVAL() / MAX_HEARTBEAT_INTERVAL() Inclusive lower and upper bounds, in seconds, for `heartbeatInterval`. Set at deployment, immutable thereafter. ```solidity function MIN_HEARTBEAT_INTERVAL() external view returns (uint256); function MAX_HEARTBEAT_INTERVAL() external view returns (uint256); ``` ### pauseDuration() Current pause duration, in seconds, applied to a pausable on a successful trigger. ```solidity function pauseDuration() external view returns (uint256); ``` ### heartbeatInterval() Current heartbeat interval, in seconds. The window after a heartbeat during which the pauser remains authorized. ```solidity function heartbeatInterval() external view returns (uint256); ``` ### heartbeatExpiry() Returns the timestamp after which the given pauser is no longer authorized to heartbeat or pause. ```solidity function heartbeatExpiry(address pauser) external view returns (uint256); ``` #### Parameters | Name | Type | Description | |----------|-----------|----------------------------| | `pauser` | `address` | Pauser address to look up. | ### getPauser() Returns the pauser currently registered for a pausable, or the zero address if none. ```solidity function getPauser(address _pausable) external view returns (address); ``` #### Parameters | Name | Type | Description | |-------------|-----------|-----------------------------| | `_pausable` | `address` | Pausable contract address. | ### getPausables() Returns all pausable addresses currently registered. ```solidity function getPausables() external view returns (address[] memory); ``` ### getPausableCount() Returns the number of pausables assigned to a pauser. ```solidity function getPausableCount(address _pauser) external view returns (uint256); ``` #### Parameters | Name | Type | Description | |-----------|-----------|-------------------| | `_pauser` | `address` | Pauser address. | ### isPauserLive() Returns whether the pauser's heartbeat has not expired. ```solidity function isPauserLive(address _pauser) external view returns (bool); ``` #### Parameters | Name | Type | Description | |-----------|-----------|-------------------| | `_pauser` | `address` | Pauser address. | Returns `true` when `block.timestamp < heartbeatExpiry[_pauser]`. ## Write Methods ### Admin methods The following methods can be called only by `ADMIN`. They revert with `SenderNotAdmin` otherwise. #### setPauseDuration() Sets the pause duration applied on subsequent triggers. The new value takes effect immediately for any pauses called afterward. ```solidity function setPauseDuration(uint256 _newPauseDuration) external; ``` #### Parameters | Name | Type | Description | |---------------------|-----------|--------------------------------------------| | `_newPauseDuration` | `uint256` | New pause duration, in seconds. | :::note Reverts if any of the following is true: - caller is not `ADMIN` (`SenderNotAdmin`) - `_newPauseDuration < MIN_PAUSE_DURATION` (`PauseDurationBelowMin`) - `_newPauseDuration > MAX_PAUSE_DURATION` (`PauseDurationAboveMax`) ::: Emits `PauseDurationUpdated(previousPauseDuration, newPauseDuration)`. #### setHeartbeatInterval() Sets the heartbeat interval pausers must maintain to remain authorized. The new value applies only to subsequent heartbeats and registrations; already-stored `heartbeatExpiry` values are not changed retroactively. ```solidity function setHeartbeatInterval(uint256 _newHeartbeatInterval) external; ``` #### Parameters | Name | Type | Description | |-------------------------|-----------|--------------------------------------| | `_newHeartbeatInterval` | `uint256` | New heartbeat interval, in seconds. | :::note Reverts if any of the following is true: - caller is not `ADMIN` (`SenderNotAdmin`) - `_newHeartbeatInterval < MIN_HEARTBEAT_INTERVAL` (`HeartbeatIntervalBelowMin`) - `_newHeartbeatInterval > MAX_HEARTBEAT_INTERVAL` (`HeartbeatIntervalAboveMax`) ::: Emits `HeartbeatIntervalUpdated(previousHeartbeatInterval, newHeartbeatInterval)`. #### registerPauser() Registers, replaces, or unregisters a pauser for a pausable. - The previous pauser, if any, is overwritten. If they are left with zero remaining pausables, their `heartbeatExpiry` is cleared to `0`. - The new pauser's `heartbeatExpiry` is set to `block.timestamp + heartbeatInterval` (extending or initializing it). - Passing `address(0)` as `_newPauser` unregisters the pausable's current pauser. ```solidity function registerPauser(address _pausable, address _newPauser) external; ``` #### Parameters | Name | Type | Description | |--------------|-----------|----------------------------------------------------------------------| | `_pausable` | `address` | Pausable contract address. | | `_newPauser` | `address` | New pauser address. Zero unregisters the current pauser, if any. | :::note - Reverts if caller is not `ADMIN` (`SenderNotAdmin`). - Does **not** verify that CircuitBreaker holds the pause role on `_pausable`, or that `_pausable` implements `IPausable`. The DAO is responsible for ensuring these invariants when assigning pausers. ::: Emits `HeartbeatUpdated` for the previous pauser (if their expiry was cleared) and for the new pauser. ### Pauser methods #### heartbeat() Records a liveness proof, extending the caller's `heartbeatExpiry` to `block.timestamp + heartbeatInterval`. ```solidity function heartbeat() external; ``` :::note Reverts if any of the following is true: - caller is not registered as a pauser for any pausable (`SenderNotPauser`) - caller's heartbeat has already expired (`HeartbeatExpired`) โ€” a lapsed pauser cannot self-renew; the DAO must explicitly re-register them ::: Emits `HeartbeatUpdated(pauser, newHeartbeatExpiry)`. #### pause() Pauses a registered pausable for the current `pauseDuration`. Single-use: the caller is unregistered from this pausable on success. ```solidity function pause(address _pausable) external; ``` The target must implement the minimal `IPausable` interface that CircuitBreaker calls into: ```solidity interface IPausableUntil { function isPaused() external view returns (bool); function pauseFor(uint256 _duration) external; } ``` #### Parameters | Name | Type | Description | |-------------|-----------|-----------------------------------| | `_pausable` | `address` | Pausable contract to pause. | The execution flow is: 1. Verify `msg.sender` is the registered pauser of `_pausable` and is live. 2. Unregister `msg.sender` from `_pausable`. 3. Call `IPausable(_pausable).pauseFor(pauseDuration)`. 4. Verify `IPausable(_pausable).isPaused()` is `true`. 5. Update the caller's `heartbeatExpiry`: extended to `block.timestamp + heartbeatInterval` if any other pausables are still assigned to them, or cleared to `0` otherwise. :::note Reverts if any of the following is true: - caller is not the registered pauser of `_pausable` (`SenderNotPauser`) - caller's heartbeat has expired (`HeartbeatExpired`) - the target does not report itself paused after `pauseFor()` call (`PauseFailed`) - the call reentered (`ReentrantCall`) ::: Emits `PauseTriggered(pausable, pauser, pauseDuration)` and `HeartbeatUpdated(pauser, newHeartbeatExpiry)`. ## Events ```solidity event CircuitBreakerInitialized( address indexed admin, uint256 minPauseDuration, uint256 maxPauseDuration, uint256 minHeartbeatInterval, uint256 maxHeartbeatInterval ); ``` Emitted once at construction with the immutable admin and bounds. ```solidity event PauseDurationUpdated(uint256 previousPauseDuration, uint256 newPauseDuration); ``` Emitted on `setPauseDuration` and once at construction for the initial value. ```solidity event HeartbeatIntervalUpdated(uint256 previousHeartbeatInterval, uint256 newHeartbeatInterval); ``` Emitted on `setHeartbeatInterval` and once at construction for the initial value. ```solidity event HeartbeatUpdated(address indexed pauser, uint256 newHeartbeatExpiry); ``` Emitted whenever a pauser's heartbeat expiry changes โ€” on `heartbeat()`, `pause()`, and `registerPauser()`. ```solidity event PauseTriggered(address indexed pausable, address indexed pauser, uint256 pauseDuration); ``` Emitted on a successful `pause()`. --- # ConsolidationBus - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/consolidation/ConsolidationBus.sol) - [Deployed contract](https://etherscan.io/address/0xd907CE33B4Be423823d1CFFe80BD147E8b8554C8) - Specification basis: [LIP-35 โ€” Staking Router v3](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md) ConsolidationBus is a message bus for [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) consolidation requests that decouples batch publication from execution and fee payment. Trusted publishers announce consolidation batches on-chain, and after a configurable delay anyone can execute a published batch by supplying the required fee. ## What is ConsolidationBus The bus is the middle link of the [Lido Core consolidation pipeline](/contracts/consolidation-gateway#consolidation-flow-in-lido-core): it receives validated consolidation batches from the [ConsolidationMigrator](/contracts/consolidation-migrator) and forwards them to the [ConsolidationGateway](/contracts/consolidation-gateway) for execution. The flow is two-step: 1. **Publish.** A registered publisher (`PUBLISH_ROLE`, intended for the ConsolidationMigrator) publishes a batch of consolidation groups via [`addConsolidationRequests`](#addconsolidationrequests). The batch is validated and stored as a hash together with the submission timestamp โ€” no fee is paid at this stage. 2. **Execute.** After the [execution delay](#execution-delay) has elapsed, anyone can call [`executeConsolidation`](#executeconsolidation), supplying the matching batch extended with target validator witnesses and the required ether fee. The bus recomputes the batch hash, verifies the delay, and forwards the batch to the gateway, passing the executor as the fee refund recipient. This separation serves two purposes. First, batches can be published by a trusted on-chain party while the consolidation fee is paid by an independent, permissionless executor (a dedicated Consolidation Executor bot is expected to monitor the bus and execute pending batches). Second, the delay between publication and execution creates a time window during which an erroneous or malicious batch can be [removed](#removebatches) from the queue or blocked by pausing [DepositSecurityModule](/contracts/deposit-security-module) deposits (see [Execution delay](#execution-delay)). The contract inherits `AccessControlEnumerableUpgradeable` and is deployed behind an [OssifiableProxy](/contracts/ossifiable-proxy). ## Batch lifecycle ### Publication A batch is an array of consolidation groups, each mapping several source validator public keys to a single target validator public key: ```solidity struct ConsolidationGroup { bytes[] sourcePubkeys; bytes targetPubkey; } ``` On publication via [`addConsolidationRequests`](#addconsolidationrequests), the bus validates that: - the batch is not empty and the number of groups does not exceed [`maxGroupsInBatch`](#maxgroupsinbatch); - every group contains at least one source key, and the total number of source keys across the batch does not exceed [`batchSize`](#batchsize); - every source and target public key is 48 bytes long, and no source key equals its group's target key; - an identical batch is not already pending. Only the batch hash โ€” `keccak256(abi.encode(groups))` โ€” is stored on-chain, along with the publisher address and the submission timestamp: ```solidity struct BatchInfo { address publisher; uint64 addedAt; } ``` The full encoded batch data is emitted in the [`RequestsAdded`](#requestsadded) event, so executors can reconstruct pending batches from event logs. The same batch cannot be pending twice, but it can be published again after it has been executed or removed. ### Execution Execution via [`executeConsolidation`](#executeconsolidation) is permissionless and payable. The executor supplies the batch in the gateway's format โ€” each group's target key replaced with a [`ValidatorWitness`](/contracts/predeposit-guarantee#validatorwitness) carrying the target's withdrawal credentials proof. The bus reconstructs the published batch from the witness groups (taking each target public key from its witness), recomputes the hash, and verifies that: - the batch is pending โ€” it was published and has not been executed or removed; - the [execution delay](#execution-delay) has elapsed since publication. The batch is then deleted from the queue and forwarded to [`ConsolidationGateway.addConsolidationRequests`](/contracts/consolidation-gateway#addconsolidationrequests) together with the attached ether, with the executor (`msg.sender`) set as the refund recipient for any fee excess. The bus itself does not re-validate the requests' contents at execution time โ€” hash equality guarantees the batch is byte-for-byte the one published. All execution-time checks (withdrawal credentials proofs, rate limits, protocol state, fee sufficiency) are performed by the [gateway](/contracts/consolidation-gateway#request-processing). If the gateway reverts, the whole execution transaction reverts and the batch remains pending, so execution can be retried later. ### Removal An account with the `REMOVE_ROLE` can remove pending batches from the queue by hash via [`removeBatches`](#removebatches). This is the safety hatch for the window between publication and execution: a batch published in error can be withdrawn before any executor pays for it. ### Execution delay The delay between publication and execution, in seconds, readable via [`executionDelay`](#executiondelay) and configurable via [`setExecutionDelay`](#setexecutiondelay) (zero means batches are executable immediately). The delay ensures that honest [DepositSecurityModule](/contracts/deposit-security-module) guardians have sufficient time to pause deposits โ€” which blocks consolidation execution at the [gateway](/contracts/consolidation-gateway#protocol-state-checks) โ€” in case keys with invalid withdrawal credentials were deposited. The delay is a single global parameter and is not snapshotted per batch: changing it applies retroactively to all pending batches. `MANAGE_ROLE` holders are trusted with this behavior. ## Roles Access to lever methods is restricted using the functionality of the `AccessControlEnumerableUpgradeable` contract: - `DEFAULT_ADMIN_ROLE` โ€” manages role assignments; held by the Lido DAO Aragon Agent; - `PUBLISH_ROLE` โ€” allows publishing consolidation batches; intended for the [ConsolidationMigrator](/contracts/consolidation-migrator); - `MANAGE_ROLE` โ€” allows configuring the batch limits and the execution delay; - `REMOVE_ROLE` โ€” allows removing pending batches from the queue. Executing a pending batch requires no role: [`executeConsolidation`](#executeconsolidation) is permissionless. ## View methods ### `batchSize` Returns the maximum total number of source keys allowed in a single batch. ```solidity function batchSize() external view returns (uint256); ``` ### `maxGroupsInBatch` Returns the maximum number of consolidation groups allowed in a single batch. Cannot exceed [`batchSize`](#batchsize). ```solidity function maxGroupsInBatch() external view returns (uint256); ``` ### `executionDelay` Returns the current [execution delay](#execution-delay) in seconds. ```solidity function executionDelay() external view returns (uint256); ``` ### `getConsolidationGateway` Returns the address of the [ConsolidationGateway](/contracts/consolidation-gateway) the bus forwards batches to. The address is immutable, set at deployment. ```solidity function getConsolidationGateway() external view returns (address); ``` ### `getBatchInfo` Returns the stored info for a pending batch: the publisher address and the publication timestamp. Both fields are zero if the batch is not in the queue (never published, already executed, or removed). ```solidity function getBatchInfo(bytes32 batchHash) external view returns (BatchInfo memory); ``` **Parameters:** | Name | Type | Description | | ----------- | --------- | ------------------------------- | | `batchHash` | `bytes32` | hash of the batch to look up | **Returns:** | Name | Type | Description | | ---- | ----------- | ------------------------------------------------------------------------------- | | | `BatchInfo` | publisher address and `addedAt` timestamp; zero values if the batch is not pending | ## Write methods ### `addConsolidationRequests` Publishes a batch of grouped consolidation requests to the queue. See [Publication](#publication) for the validation rules. Restricted to the `PUBLISH_ROLE` role, intended for the [ConsolidationMigrator](/contracts/consolidation-migrator). ```solidity function addConsolidationRequests(ConsolidationGroup[] calldata groups) external; ``` **Parameters:** | Name | Type | Description | | -------- | ---------------------- | -------------------------------------------------------------------------------- | | `groups` | `ConsolidationGroup[]` | consolidation groups, each containing source public keys and a target public key | **Reverts:** - if the caller does not have the `PUBLISH_ROLE`; - with `EmptyBatch` if the batch is empty, and with `EmptyGroup` if any group has no source keys; - with `TooManyGroups` or `BatchTooLarge` if the batch exceeds the [limits](#publication); - with `InvalidSourcePubkeyLength` or `InvalidTargetPubkeyLength` if any public key is not 48 bytes; - with `SourceEqualsTarget` if any source key equals its group's target key; - with `BatchAlreadyPending` if an identical batch is already in the queue. ### `executeConsolidation` Executes a pending batch of grouped consolidation requests, forwarding it to the [ConsolidationGateway](/contracts/consolidation-gateway) with the attached ether as the fee and the caller as the refund recipient. See [Execution](#execution) for the detailed flow. Permissionless. ```solidity function executeConsolidation( IConsolidationGateway.ConsolidationWitnessGroup[] calldata groups ) external payable; ``` **Parameters:** | Name | Type | Description | | -------- | ----------------------------- | --------------------------------------------------------------------------------------------- | | `groups` | `ConsolidationWitnessGroup[]` | consolidation witness groups, each containing source public keys and a target validator witness | **Reverts:** - with `BatchNotFound` if the reconstructed batch was never published, was already executed, or was removed; - with `ExecutionDelayNotPassed` if the [execution delay](#execution-delay) has not elapsed since publication; - with any error propagated from the [gateway's request processing](/contracts/consolidation-gateway#request-processing) (insufficient fee, rate limit, protocol state, proof verification). ### `removeBatches` Removes pending batches from the queue by hash. See [Removal](#removal). Restricted to the `REMOVE_ROLE` role. ```solidity function removeBatches(bytes32[] calldata batchHashes) external; ``` **Parameters:** | Name | Type | Description | | ------------- | ----------- | ---------------------------------- | | `batchHashes` | `bytes32[]` | hashes of the batches to remove | **Reverts:** - if the caller does not have the `REMOVE_ROLE`; - with `EmptyBatchHashes` if the array is empty; - with `BatchNotFound` if any batch is not pending. ### `setBatchSize` Sets the maximum total number of source keys allowed in a single batch. Restricted to the `MANAGE_ROLE` role. ```solidity function setBatchSize(uint256 limit) external; ``` **Parameters:** | Name | Type | Description | | ------- | --------- | ------------------------------------------------------------------------ | | `limit` | `uint256` | new batch size limit; must be non-zero and not below `maxGroupsInBatch` | ### `setMaxGroupsInBatch` Sets the maximum number of consolidation groups allowed in a single batch. Restricted to the `MANAGE_ROLE` role. ```solidity function setMaxGroupsInBatch(uint256 limit) external; ``` **Parameters:** | Name | Type | Description | | ------- | --------- | --------------------------------------------------------------------- | | `limit` | `uint256` | new groups limit; must be non-zero and must not exceed `batchSize` | ### `setExecutionDelay` Sets the [execution delay](#execution-delay) between publishing and executing a batch. The new value applies retroactively to all pending batches. Restricted to the `MANAGE_ROLE` role. ```solidity function setExecutionDelay(uint256 delay) external; ``` **Parameters:** | Name | Type | Description | | ------- | --------- | ------------------------------------------------ | | `delay` | `uint256` | new execution delay in seconds (0 means no delay) | ## Events ### `RequestsAdded` Emitted when a batch of consolidation requests is published. `batchData` contains the full ABI-encoded batch (`abi.encode(groups)`), allowing executors to reconstruct it off-chain. ```solidity event RequestsAdded(address indexed publisher, bytes batchData); ``` ### `RequestsExecuted` Emitted when a pending batch is executed and forwarded to the gateway. ```solidity event RequestsExecuted(bytes32 indexed batchHash, uint256 feePaid); ``` ### `BatchesRemoved` Emitted when pending batches are removed from the queue. ```solidity event BatchesRemoved(bytes32[] batchHashes); ``` ### `BatchLimitUpdated` Emitted when the batch size limit is updated. ```solidity event BatchLimitUpdated(uint256 newLimit); ``` ### `MaxGroupsInBatchUpdated` Emitted when the groups-per-batch limit is updated. ```solidity event MaxGroupsInBatchUpdated(uint256 newLimit); ``` ### `ExecutionDelayUpdated` Emitted when the execution delay is updated. ```solidity event ExecutionDelayUpdated(uint256 newDelay); ``` --- # ConsolidationGateway - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/consolidation/ConsolidationGateway.sol) - [Deployed contract](https://etherscan.io/address/0x17be979344f2c2cC806229a532D92f8742C10462) - Specification basis: [LIP-35 โ€” Staking Router v3](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md) ConsolidationGateway is the single entry point for [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) validator consolidation requests in Lido Core. It proxies consolidation requests to the [WithdrawalVault](/contracts/withdrawal-vault), checking permissions, verifying target validators' withdrawal credentials, applying rate limits, and refunding any excess fee. ## What is ConsolidationGateway A consolidation is an [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) operation that merges a source validator's balance into a target `0x02` (compounding) validator. A consolidation request must be signed by the source validator's withdrawal address, and since Lido Core validators use [WithdrawalVault](/contracts/withdrawal-vault) withdrawal credentials, requests to the EIP-7251 system contract are submitted from the WithdrawalVault. The vault, in turn, accepts consolidation requests only from ConsolidationGateway. Placing this logic in a dedicated contract rather than in the WithdrawalVault itself allows the consolidation flow to be paused independently in an emergency, without affecting protocol withdrawals or [triggerable exits](/contracts/triggerable-withdrawals-gateway). The contract inherits OpenZeppelin `AccessControlEnumerable`, [PausableUntil](https://github.com/lidofinance/core/blob/v4.0.0/contracts/common/utils/PausableUntil.sol), and [CLProofVerifier](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/predeposit_guarantee/CLProofVerifier.sol). It is not deployed behind a proxy. :::note ConsolidationGateway serves the Lido Core consolidation flow. For consolidations into stVaults, see [ValidatorConsolidationRequests](/contracts/validator-consolidation-requests). ::: ## Consolidation flow in Lido Core The gateway is the last permissioned step of the consolidation pipeline introduced in [LIP-35](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md) to migrate stake between staking modules (initially, from Curated Module v1 to Curated Module v2): 1. A node operator obtains permission to migrate stake via an [EasyTrack](https://dao.lido.fi/easy-track/motions) motion that allowlists a (source operator โ†’ target operator) pair with a designated Consolidation Manager address on the [ConsolidationMigrator](/contracts/consolidation-migrator). 2. The Consolidation Manager submits validator key indices to the Migrator, which validates them, resolves them to public keys via the staking modules, and publishes the batch to the [ConsolidationBus](/contracts/consolidation-bus). 3. After the bus's [execution delay](/contracts/consolidation-bus#execution-delay) has elapsed, a permissionless executor calls `ConsolidationBus.executeConsolidation`, supplying the published batch together with withdrawal credentials proofs for the target validators and the required fee. The bus forwards the batch to the gateway. 4. The gateway validates the request (see [Request processing](#request-processing)) and forwards it, together with the exact required fee, to [`WithdrawalVault.addConsolidationRequests`](/contracts/withdrawal-vault#addconsolidationrequests), which submits one EIP-7251 request per (source, target) pair to the system contract. ```mermaid graph TD; A[EasyTrack motion] -->|allowPair| B[ConsolidationMigrator]; B -->|publish batch| C[ConsolidationBus]; C -->|"execute batch (delay passed, fee paid)"| D[ConsolidationGateway]; D -->|requests + exact fee| E[WithdrawalVault]; E --> F[EIP-7251 system contract]; ``` ## Request processing [`addConsolidationRequests`](#addconsolidationrequests) accepts consolidation requests grouped by target validator: each group contains the source validators' public keys and a [`ValidatorWitness`](/contracts/predeposit-guarantee#validatorwitness) proving the shared target validator's withdrawal credentials. Before forwarding anything to the WithdrawalVault, the gateway enforces the following: 1. **Access.** The caller holds `ADD_CONSOLIDATION_REQUEST_ROLE` (intended for the [ConsolidationBus](/contracts/consolidation-bus)), and the contract is not paused. 2. **Well-formedness.** A non-zero fee is attached, the batch is not empty, and every group contains at least one source key. 3. **Protocol state.** Deposits are not paused on the [DepositSecurityModule](/contracts/deposit-security-module), and [`Lido.canDeposit()`](/contracts/lido#candeposit) returns true (i.e., the protocol is not stopped and bunker mode is not active). See [Protocol state checks](#protocol-state-checks). 4. **Target withdrawal credentials.** Every target validator's withdrawal credentials are proven on-chain to match the Lido `0x02` withdrawal credentials. See [Target validator verification](#target-validator-verification). 5. **Rate limit.** The number of individual requests in the batch (the total count of source keys) fits into the current frame's quota. See [Consolidation limits](#consolidation-limits). 6. **Fee.** The attached ether covers `requestsCount ร— WithdrawalVault.getConsolidationRequestFee()`. Exactly this amount is forwarded to the vault, and any excess is refunded to the `refundRecipient` (or to the caller if the recipient is the zero address). The entire call is wrapped in a balance-preservation check (the `preservesEthBalance` modifier): the gateway never accumulates ether โ€” everything it receives is either forwarded to the WithdrawalVault or refunded within the same transaction. ### Protocol state checks Consolidation requests are blocked while [DepositSecurityModule](/contracts/deposit-security-module) deposits are paused. If the DSM has paused deposits, some recently deposited validators may not belong to Lido and can therefore have non-Lido withdrawal credentials; blocking consolidations in this state acts as an additional safety check on top of the withdrawal credentials proof verification. Combined with the [execution delay](/contracts/consolidation-bus#execution-delay) on the ConsolidationBus, this gives honest DSM guardians time to pause deposits โ€” and thereby block pending consolidations โ€” if keys with invalid withdrawal credentials were deposited. For the same reason, requests are also blocked while the protocol is not operating normally: if Lido is stopped or bunker mode is active (`Lido.canDeposit()` returns false), the call reverts. ### Target validator verification A consolidation credits the source validator's balance to the target validator, so the gateway must ensure the target is controlled by the protocol. The expected withdrawal credentials are derived on the fly as the `0x02` prefix joined with the [WithdrawalVault](/contracts/withdrawal-vault) address: ``` withdrawalCredentials = 0x02 | 11 zero bytes | 20-byte WithdrawalVault address ``` For each group, the target validator's actual withdrawal credentials are verified against this value using an on-chain Merkle proof of the validator container in the Consensus Layer state, anchored to a beacon block root obtained via [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) (the `CLProofVerifier` base contract). Source validators are not verified by the gateway: an EIP-7251 consolidation request is only actionable by the Consensus Layer if the source validator actually has the protocol's withdrawal credentials, so an invalid source key results in a no-op request. Source keys are additionally validated upstream by the [ConsolidationMigrator](/contracts/consolidation-migrator) and [ConsolidationBus](/contracts/consolidation-bus). ### Consolidation limits To prevent overload, the gateway rate-limits the number of consolidation requests using a replenishing quota (the same `RateLimit` mechanism as the [TriggerableWithdrawalsGateway](/contracts/triggerable-withdrawals-gateway) exit limits): - `maxConsolidationRequestsLimit` โ€” the quota ceiling; - `consolidationsPerFrame` โ€” how many requests are restored to the quota per frame; - `frameDurationInSec` โ€” the frame duration in seconds. Each processed request consumes one unit of the quota, and the quota is restored at a rate of `consolidationsPerFrame` per frame up to the ceiling. If a batch does not fit into the remaining quota, the whole call reverts with `ConsolidationRequestsLimitExceeded`. Setting `maxConsolidationRequestsLimit` to zero disables limiting entirely. The limit parameters are set at deployment and can be updated via [`setConsolidationRequestLimit`](#setconsolidationrequestlimit); the current state is readable via [`getConsolidationRequestLimitFullInfo`](#getconsolidationrequestlimitfullinfo). ## Roles Access to lever methods is restricted using the functionality of the OpenZeppelin `AccessControlEnumerable` contract: - `DEFAULT_ADMIN_ROLE` โ€” manages role assignments; held by the Lido DAO Aragon Agent; - `ADD_CONSOLIDATION_REQUEST_ROLE` โ€” allows submitting consolidation requests; intended for the [ConsolidationBus](/contracts/consolidation-bus); - `EXIT_LIMIT_MANAGER_ROLE` โ€” allows updating the [consolidation limits](#consolidation-limits); - `PAUSE_ROLE` / `RESUME_ROLE` โ€” allow pausing and resuming the contract (see [PausableUntil](https://github.com/lidofinance/core/blob/v4.0.0/contracts/common/utils/PausableUntil.sol)). ## View methods ### `getConsolidationRequestLimitFullInfo` Returns the full state of the [consolidation limits](#consolidation-limits). ```solidity function getConsolidationRequestLimitFullInfo() external view returns ( uint256 maxConsolidationRequestsLimit, uint256 consolidationsPerFrame, uint256 frameDurationInSec, uint256 prevConsolidationRequestsLimit, uint256 currentConsolidationRequestsLimit ); ``` **Returns:** | Name | Type | Description | | ----------------------------------- | --------- | ----------------------------------------------------------------------------------------------- | | `maxConsolidationRequestsLimit` | `uint256` | maximum number of consolidation requests (the quota ceiling) | | `consolidationsPerFrame` | `uint256` | number of requests restored to the quota per frame | | `frameDurationInSec` | `uint256` | frame duration in seconds | | `prevConsolidationRequestsLimit` | `uint256` | quota left after the previous requests | | `currentConsolidationRequestsLimit` | `uint256` | current quota; `type(uint256).max` if the limit is not set | ## Write methods ### `addConsolidationRequests` Submits grouped consolidation requests to the [WithdrawalVault](/contracts/withdrawal-vault). Each group represents multiple source validators consolidating into a single target validator, whose withdrawal credentials are verified on-chain. Any excess ether beyond the exact required fee is refunded to `refundRecipient`. See [Request processing](#request-processing) for the detailed flow. Restricted to the `ADD_CONSOLIDATION_REQUEST_ROLE` role, intended for the [ConsolidationBus](/contracts/consolidation-bus). ```solidity function addConsolidationRequests( ConsolidationWitnessGroup[] calldata groups, address refundRecipient ) external payable; ``` **Parameters:** | Name | Type | Description | | ----------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `groups` | `ConsolidationWitnessGroup[]` | consolidation groups, each containing source public keys and a target validator witness with a withdrawal credentials proof | | `refundRecipient` | `address` | address to receive any excess ether sent for fees; if zero, the excess is refunded to the caller | **Reverts:** - if the caller does not have the `ADD_CONSOLIDATION_REQUEST_ROLE` or the contract is paused; - with `ZeroArgument` if no ether is attached or `groups` is empty, and with `EmptyGroup` if any group has no source keys; - with `DSMDepositsPaused` or `LidoDepositsPaused` if the [protocol state checks](#protocol-state-checks) fail; - if any target validator's withdrawal credentials proof fails verification; - with `ConsolidationRequestsLimitExceeded` if the batch does not fit into the current frame's quota; - with `InsufficientFee` if the attached ether does not cover the total fee, and with `FeeRefundFailed` if the excess fee refund fails. ### `setConsolidationRequestLimit` Updates the [consolidation limits](#consolidation-limits): the quota ceiling and the pace at which the quota is restored. Restricted to the `EXIT_LIMIT_MANAGER_ROLE` role. ```solidity function setConsolidationRequestLimit( uint256 maxConsolidationRequestsLimit, uint256 consolidationsPerFrame, uint256 frameDurationInSec ) external; ``` **Parameters:** | Name | Type | Description | | ------------------------------- | --------- | ----------------------------------------------------- | | `maxConsolidationRequestsLimit` | `uint256` | maximum number of consolidation requests; zero disables limiting | | `consolidationsPerFrame` | `uint256` | number of requests restored to the quota per frame | | `frameDurationInSec` | `uint256` | frame duration in seconds | ### `pauseFor` Pauses the contract for the specified duration, blocking new consolidation requests. Restricted to the `PAUSE_ROLE` role. ```solidity function pauseFor(uint256 _duration) external; ``` **Parameters:** | Name | Type | Description | | ----------- | --------- | ---------------------------------------------------------------- | | `_duration` | `uint256` | pause duration in seconds (use `PAUSE_INFINITELY` for unlimited) | ### `pauseUntil` Pauses the contract until the specified timestamp (inclusive). Restricted to the `PAUSE_ROLE` role. ```solidity function pauseUntil(uint256 _pauseUntilInclusive) external; ``` **Parameters:** | Name | Type | Description | | ---------------------- | --------- | ----------------------------------------- | | `_pauseUntilInclusive` | `uint256` | the last second to pause until, inclusive | ### `resume` Resumes the contract. Restricted to the `RESUME_ROLE` role. ```solidity function resume() external; ``` ## Events ### `ConsolidationRequestsLimitSet` Emitted when the [consolidation limits](#consolidation-limits) are set or updated. ```solidity event ConsolidationRequestsLimitSet( uint256 maxConsolidationRequestsLimit, uint256 consolidationsPerFrame, uint256 frameDurationInSec ); ``` --- # ConsolidationMigrator - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/consolidation/ConsolidationMigrator.sol) - [Deployed contract](https://etherscan.io/address/0x9Dc70b5A4f4F5E4AF9058C983D560564F031f1D7) - Specification basis: [LIP-35 โ€” Staking Router v3](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md) ConsolidationMigrator is a temporary helper contract for migrating stake between staking modules via [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) consolidations. It validates consolidation requests submitted as validator key indices, resolves them to public keys through the staking modules, and publishes the resulting batches to the [ConsolidationBus](/contracts/consolidation-bus). ## What is ConsolidationMigrator The migrator is the entry point of the [Lido Core consolidation pipeline](/contracts/consolidation-gateway#consolidation-flow-in-lido-core). Its primary purpose is to support the stake migration from legacy staking modules to new ones โ€” initially, from Curated Module v1 (CMv1) to Curated Module v2 (CMv2), as described in [LIP-33](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-33.md) and [LIP-35](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md). Each migrator instance is bound to a fixed **source** staking module and a fixed **target** staking module: both module IDs are immutable, set in the implementation constructor and readable via [`sourceModuleId`](#sourcemoduleid) and [`targetModuleId`](#targetmoduleid). Consolidations are permitted only for explicitly allowed (source operator โ†’ target operator) pairs. Each allowed pair has a designated **Consolidation Manager** (submitter) โ€” the only address authorized to submit consolidation batches for that pair. The allowlist is managed through an [EasyTrack](https://dao.lido.fi/easy-track/motions)-driven flow (see [Allowlist management](#allowlist-management)). The contract inherits `AccessControlEnumerableUpgradeable` and is deployed behind an [OssifiableProxy](/contracts/ossifiable-proxy). ## Migration flow 1. A node operator creates an EasyTrack motion specifying a single source operator ID (in the source module), a list of target operator IDs (in the target module), and a single Consolidation Manager address. Upon enactment, [`allowPair`](#allowpair) is called for each (source, target) pair. 2. The Consolidation Manager calls [`submitConsolidationBatch`](#submitconsolidationbatch) with validator key indices grouped by target key: each group maps several source key indices to a single target key index. 3. The migrator validates the batch (see [Batch validation](#batch-validation)) and resolves the key indices to validator public keys through the source and target staking modules. 4. The resulting groups are published to the [ConsolidationBus](/contracts/consolidation-bus) (the migrator is expected to hold the bus's `PUBLISH_ROLE`), from where the batch proceeds through the standard execution flow: [ConsolidationBus](/contracts/consolidation-bus) โ†’ [ConsolidationGateway](/contracts/consolidation-gateway) โ†’ [WithdrawalVault](/contracts/withdrawal-vault). ### Batch validation A batch is an array of index groups: ```solidity struct ConsolidationIndexGroup { uint256[] sourceKeyIndices; uint256 targetKeyIndex; } ``` [`submitConsolidationBatch`](#submitconsolidationbatch) verifies that: - the caller is the designated submitter (Consolidation Manager) for the (source operator โ†’ target operator) pair โ€” which also implies the pair is currently allowed; - every source key index refers to a deposited key of the source operator in the source module; - the target key index of every group refers to a deposited key of the target operator in the target module. A key index is considered deposited if it is less than the operator's `totalDepositedValidators` counter obtained via the module's `getNodeOperatorSummary`. The public keys themselves are fetched via the module's `getSigningKeys` (see [Module interaction](#module-interaction)). Batch-level constraints โ€” the batch size limit, the groups-per-batch limit, key length, and source-not-equal-to-target checks โ€” are enforced downstream by the [ConsolidationBus](/contracts/consolidation-bus#publication). ### Module interaction Since the migrator is a temporary contract, it relies on the key-retrieval methods already implemented by the supported modules through a `getSigningKeys` method. ### Allowlist management Consolidation pairs are managed with three methods: - [`allowPair`](#allowpair) โ€” allows consolidation from a source operator to a target operator with a designated submitter. Called via an EasyTrack motion upon enactment. Calling it again for an existing pair updates the submitter. - [`disallowPair`](#disallowpair) โ€” disallows a pair and removes its submitter. May be used to correct an incorrectly allowed pair, update the Consolidation Manager address, or stop a consolidation for operational reasons. - [`selfDisallowPair`](#selfdisallowpair) โ€” lets the current submitter revoke their own pair without any role. Once a pair is disallowed, it can be allowed again only through a new EasyTrack motion. ## Roles Access to lever methods is restricted using the functionality of the `AccessControlEnumerableUpgradeable` contract: - `DEFAULT_ADMIN_ROLE` โ€” manages role assignments; held by the Lido DAO Aragon Agent; - `ALLOW_PAIR_ROLE` โ€” allows adding consolidation pairs to the allowlist; intended for the [EasyTrack](https://dao.lido.fi/easy-track/motions) executor; - `DISALLOW_PAIR_ROLE` โ€” allows removing consolidation pairs from the allowlist. Submitting a batch requires no role โ€” [`submitConsolidationBatch`](#submitconsolidationbatch) is restricted to the pair's designated submitter instead. ## View methods ### `isPairAllowed` Returns whether consolidation from the source operator to the target operator is allowed. ```solidity function isPairAllowed(uint256 sourceOperatorId, uint256 targetOperatorId) external view returns (bool); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | -------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | | `targetOperatorId` | `uint256` | id of the target operator in the target module | ### `getAllowedTargets` Returns all allowed target operators for the given source operator. ```solidity function getAllowedTargets(uint256 sourceOperatorId) external view returns (uint256[] memory); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | -------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | **Returns:** | Name | Type | Description | | ---- | ----------- | ----------------------------------- | | | `uint256[]` | ids of the allowed target operators | ### `getSubmitter` Returns the submitter (Consolidation Manager) address for a consolidation pair, or the zero address if the pair is not allowed. ```solidity function getSubmitter(uint256 sourceOperatorId, uint256 targetOperatorId) external view returns (address); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | -------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | | `targetOperatorId` | `uint256` | id of the target operator in the target module | ### `getStakingRouter` Returns the address of the [StakingRouter](/contracts/staking-router) used to resolve staking module addresses. The address is immutable, set at deployment. ```solidity function getStakingRouter() external view returns (address); ``` ### `getConsolidationBus` Returns the address of the [ConsolidationBus](/contracts/consolidation-bus) the migrator publishes batches to. The address is immutable, set at deployment. ```solidity function getConsolidationBus() external view returns (address); ``` ### `sourceModuleId` Returns the id of the source staking module this migrator is bound to. ```solidity function sourceModuleId() external view returns (uint256); ``` ### `targetModuleId` Returns the id of the target staking module this migrator is bound to. ```solidity function targetModuleId() external view returns (uint256); ``` ## Write methods ### `submitConsolidationBatch` Validates a consolidation batch submitted as key indices, resolves the indices to validator public keys, and publishes the resulting groups to the [ConsolidationBus](/contracts/consolidation-bus). See [Batch validation](#batch-validation) for the checks performed. Can be called only by the designated submitter (Consolidation Manager) for the given pair, set via [`allowPair`](#allowpair). ```solidity function submitConsolidationBatch( uint256 sourceOperatorId, uint256 targetOperatorId, ConsolidationIndexGroup[] calldata groups ) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | --------------------------- | ------------------------------------------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | | `targetOperatorId` | `uint256` | id of the target operator in the target module | | `groups` | `ConsolidationIndexGroup[]` | index groups, each containing source key indices and a single target key index | **Reverts:** - with `NotAuthorized` if the caller is not the designated submitter for the pair (including when the pair is not allowed); - with `KeyNotDeposited` if any referenced key index is not deposited; - with any error propagated from the [bus's publication checks](/contracts/consolidation-bus#addconsolidationrequests) (batch limits, key length, duplicate batch). ### `allowPair` Allows a consolidation pair (source operator โ†’ target operator) with a designated submitter. Calling it again for an existing pair updates the submitter. Restricted to the `ALLOW_PAIR_ROLE` role, intended to be called via an [EasyTrack](https://dao.lido.fi/easy-track/motions) motion upon enactment. ```solidity function allowPair(uint256 sourceOperatorId, uint256 targetOperatorId, address submitter) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ------------------------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | | `targetOperatorId` | `uint256` | id of the target operator in the target module | | `submitter` | `address` | address authorized to submit consolidation batches for the pair | **Reverts:** - if the caller does not have the `ALLOW_PAIR_ROLE`; - with `ZeroArgument` if `submitter` is the zero address. ### `disallowPair` Disallows a consolidation pair and removes its submitter. Restricted to the `DISALLOW_PAIR_ROLE` role. ```solidity function disallowPair(uint256 sourceOperatorId, uint256 targetOperatorId) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | -------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | | `targetOperatorId` | `uint256` | id of the target operator in the target module | **Reverts:** - if the caller does not have the `DISALLOW_PAIR_ROLE`; - with `PairNotInAllowlist` if the pair is not allowed. ### `selfDisallowPair` Lets the current submitter disallow their own pair. Requires no role: the caller must be the designated submitter for the pair. ```solidity function selfDisallowPair(uint256 sourceOperatorId, uint256 targetOperatorId) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | -------------------------------------------- | | `sourceOperatorId` | `uint256` | id of the source operator in the source module | | `targetOperatorId` | `uint256` | id of the target operator in the target module | **Reverts:** - with `NotAuthorized` if the caller is not the designated submitter for the pair. ## Events ### `ConsolidationPairAllowed` Emitted when a consolidation pair is allowed or its submitter is updated. ```solidity event ConsolidationPairAllowed( uint256 indexed sourceOperatorId, uint256 indexed targetOperatorId, address indexed submitter ); ``` ### `ConsolidationPairDisallowed` Emitted when a consolidation pair is disallowed, either by the `DISALLOW_PAIR_ROLE` holder or by the submitter themselves. ```solidity event ConsolidationPairDisallowed( uint256 indexed sourceOperatorId, uint256 indexed targetOperatorId, address indexed submitter ); ``` ### `ConsolidationSubmitted` Emitted when a consolidation batch is validated and published to the bus. ```solidity event ConsolidationSubmitted( uint256 indexed sourceOperatorId, uint256 indexed targetOperatorId, ConsolidationIndexGroup[] groups ); ``` --- # Dashboard - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/dashboard/Dashboard.sol) - [Implementation](https://etherscan.io/address/0x294825c2764c7D412dc32d87E2242c4f1D989AF3) Management contract for stVaults. Provides role-based control for vault operations and convenience wrappers for minting/burning stETH and wstETH. Individual Dashboard instances are deployed as proxies by [VaultFactory](/contracts/staking-vault-factory). ## What is Dashboard? Dashboard is the default control surface for stVaults: - owns the `StakingVault` on deployment - exposes role-based permissioning for vault actions - routes mint/burn and funding operations to VaultHub - manages PDG policy and unguaranteed deposits - handles node operator fee accounting and disbursement Dashboard is technically optional - advanced users can interact with `StakingVault` and `VaultHub` directly, but Dashboard provides a convenient interface with granular permission controls. ## Roles and permissions Dashboard uses OpenZeppelin's `AccessControl` with a two-admin model: **Vault Owner** and **Node Operator Manager**. Each can delegate specific sub-roles to other addresses. ### Admin roles | Role | Description | | ---------------------------- | --------------------------------------------------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | Vault Owner admin. Can grant/revoke any role, transfer vault ownership, and perform all owner operations. | | `NODE_OPERATOR_MANAGER_ROLE` | Node Operator admin. Can grant/revoke node operator sub-roles and manage operator-specific settings. | ### Vault Owner delegatable roles These roles can be granted by `DEFAULT_ADMIN_ROLE`: | Role | Operations | | ----------------------------------- | ------------------------------------------------- | | `FUND_ROLE` | Supply (fund) ETH to the stVault | | `WITHDRAW_ROLE` | Withdraw ETH from the stVault balance | | `MINT_ROLE` | Mint stETH within minting capacity | | `BURN_ROLE` | Burn stETH to decrease liability | | `REBALANCE_ROLE` | Perform voluntary rebalance | | `PAUSE_BEACON_CHAIN_DEPOSITS_ROLE` | Pause ETH deposits to Beacon Chain | | `RESUME_BEACON_CHAIN_DEPOSITS_ROLE` | Resume ETH deposits to Beacon Chain | | `REQUEST_VALIDATOR_EXIT_ROLE` | Request validator exits | | `TRIGGER_VALIDATOR_WITHDRAWAL_ROLE` | Force validator withdrawals via EIP-7002 | | `VOLUNTARY_DISCONNECT_ROLE` | Disconnect from VaultHub | | `VAULT_CONFIGURATION_ROLE` | Change tier, sync tier params, update share limit | | `COLLECT_VAULT_ERC20_ROLE` | Recover ERC-20 tokens from vault | ### Node Operator Manager delegatable roles These roles can be granted by `NODE_OPERATOR_MANAGER_ROLE`: | Role | Operations | | -------------------------------------------- | ---------------------------------------------------------- | | `NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE` | Bypass PDG and deposit directly to validators | | `NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE` | Prove unknown validators through PDG | | `NODE_OPERATOR_FEE_EXEMPT_ROLE` | Add fee exemptions to exclude value from operator fee base | ## PDGPolicy The `PDGPolicy` enum controls how deposits interact with the Predeposit Guarantee system: ```solidity enum PDGPolicy { STRICT, // All deposits require full PDG process (default) ALLOW_PROVE, // Allows proving unknown validators but not unguaranteed deposits ALLOW_DEPOSIT_AND_PROVE // Allows both unguaranteed deposits and proving unknown validators } ``` | Policy | Unguaranteed Deposits | Prove Unknown Validators | | ------------------------- | --------------------- | ------------------------ | | `STRICT` | No | No | | `ALLOW_PROVE` | No | Yes | | `ALLOW_DEPOSIT_AND_PROVE` | Yes | Yes | Set via `setPDGPolicy()` by the Vault Owner (`DEFAULT_ADMIN_ROLE`). ## Node operator fee accounting Dashboard implements a **high-water mark** approach for node operator fees based on vault growth: 1. **Growth calculation**: Growth is the difference between `totalValue` and `inOutDelta`, representing value that was not directly funded to the vault. 2. **Settled growth**: The portion of growth that fees have already been calculated on or that is exempt from fees. 3. **Fee calculation**: `fee = max(growth - settledGrowth, 0) * feeRate / 10000` 4. **Only positive growth**: Fees are only charged on new growth above the settled growth marker. If the vault value decreases (e.g., due to slashing), no fees accrue until growth exceeds the previous settled level. 5. **Quarantine inclusion**: Fee calculations include quarantined value from the LazyOracle. ### Safety threshold An **abnormally high fee threshold** (1% of total value, defined as `ABNORMALLY_HIGH_FEE_THRESHOLD_BP = 100`) exists as a safety mechanism. If calculated fees exceed this threshold, fee disbursement requires explicit action via `disburseAbnormallyHighFee()` by `DEFAULT_ADMIN_ROLE` rather than permissionless `disburseFee()`. ### Fee leftover on disconnect When a vault is voluntarily disconnected, accrued fees are collected to the Dashboard as `feeLeftover` rather than sent to `feeRecipient`. This prevents reverts from blocking disconnection. The leftover can be recovered later via `recoverFeeLeftover()`. ### Dual confirmation for critical operations The following operations require confirmation from both `DEFAULT_ADMIN_ROLE` and `NODE_OPERATOR_MANAGER_ROLE`: - `setFeeRate()` - Changing the fee rate - `correctSettledGrowth()` - Correcting the settled growth baseline - `setConfirmExpiry()` - Changing the confirmation expiry period - `transferVaultOwnership()` - Transferring vault ownership ## State variables | Variable | Type | Description | | --------------------------- | ----------- | ----------------------------------------------------- | | `feeRecipient` | `address` | Address that receives node operator fee disbursements | | `feeRate` | `uint16` | Node operator fee rate in basis points (max 10000) | | `settledGrowth` | `int128` | Growth not subject to fees (high-water mark) | | `latestCorrectionTimestamp` | `uint64` | Timestamp of most recent settled growth correction | | `feeLeftover` | `uint128` | Fees collected on disconnect, awaiting recovery | | `pdgPolicy` | `PDGPolicy` | Current PDG policy (default: STRICT) | ## View methods ### stakingVault() ```solidity function stakingVault() external view returns (IStakingVault) ``` Returns the address of the underlying StakingVault. ### vaultConnection() ```solidity function vaultConnection() public view returns (VaultHub.VaultConnection memory) ``` Returns current VaultHub connection parameters. ### liabilityShares() ```solidity function liabilityShares() public view returns (uint256) ``` Returns current liability shares. ### totalValue() ```solidity function totalValue() external view returns (uint256) ``` Returns vault total value. ### locked() ```solidity function locked() external view returns (uint256) ``` Returns locked collateral amount. ### obligations() ```solidity function obligations() external view returns (uint256 sharesToBurn, uint256 feesToSettle) ``` Returns obligations for the vault (shares to burn/rebalance and Lido fees to settle). ### healthShortfallShares() ```solidity function healthShortfallShares() external view returns (uint256) ``` Returns shares needed to restore health. Returns `UINT256_MAX` if impossible to make vault healthy using rebalance. ### obligationsShortfallValue() ```solidity function obligationsShortfallValue() external view returns (uint256) ``` Returns ETH shortfall for obligations. Returns `UINT256_MAX` if impossible to cover. ### minimalReserve() ```solidity function minimalReserve() public view returns (uint256) ``` Returns minimum reserve amount: `max(CONNECT_DEPOSIT, slashingReserve)`. This is the amount of ether locked on the vault that cannot be used for minting stETH. ### maxLockableValue() ```solidity function maxLockableValue() external view returns (uint256) ``` Returns max lockable value minus accrued node operator fee. ### totalMintingCapacityShares() ```solidity function totalMintingCapacityShares() external view returns (uint256) ``` Returns total minting capacity in shares (accounting for accrued fee). ### remainingMintingCapacityShares(uint256 \_etherToFund) ```solidity function remainingMintingCapacityShares(uint256 _etherToFund) public view returns (uint256) ``` Returns remaining minting capacity in shares for a given funding amount. ### withdrawableValue() ```solidity function withdrawableValue() public view returns (uint256) ``` Returns withdrawable ETH amount minus accrued node operator fee. ### latestReport() ```solidity function latestReport() public view returns (VaultHub.Report memory) ``` Returns the latest vault report containing `totalValue`, `inOutDelta`, and `timestamp`. ### accruedFee() ```solidity function accruedFee() public view returns (uint256 fee) ``` Returns the current node operator fee amount in ETH. ### feeRate() ```solidity function feeRate() external view returns (uint16) ``` Returns the node operator fee rate in basis points. ### feeRecipient() ```solidity function feeRecipient() external view returns (address) ``` Returns the node operator fee recipient address. ### feeLeftover() ```solidity function feeLeftover() external view returns (uint128) ``` Returns the fee amount held by the dashboard after a voluntary disconnect. ### settledGrowth() ```solidity function settledGrowth() external view returns (int128) ``` Returns the current high-water mark used for fee calculations. ### latestCorrectionTimestamp() ```solidity function latestCorrectionTimestamp() external view returns (uint64) ``` Returns the timestamp of the most recent settled growth correction. ### pdgPolicy() ```solidity function pdgPolicy() external view returns (uint8) ``` Returns the current PDG policy enum value. ### initialized() ```solidity function initialized() external view returns (bool) ``` Returns whether the dashboard has been initialized. ### confirmingRoles() ```solidity function confirmingRoles() public pure returns (bytes32[] memory roles) ``` Returns the roles that must confirm critical parameter changes (`DEFAULT_ADMIN_ROLE` and `NODE_OPERATOR_MANAGER_ROLE`). ### getConfirmExpiry() ```solidity function getConfirmExpiry() external view returns (uint256) ``` Returns the confirmation expiry period in seconds. ### confirmation(bytes \_callData, bytes32 \_role) ```solidity function confirmation(bytes memory _callData, bytes32 _role) external view returns (uint256) ``` Returns the expiry timestamp for a confirmation from `_role` for the given call data. ### hasRole(bytes32 \_role, address \_account) ```solidity function hasRole(bytes32 _role, address _account) external view returns (bool) ``` Returns whether `_account` has `_role`. ### getRoleAdmin(bytes32 \_role) ```solidity function getRoleAdmin(bytes32 _role) external view returns (bytes32) ``` Returns the admin role for `_role`. ### getRoleMember(bytes32 \_role, uint256 \_index) ```solidity function getRoleMember(bytes32 _role, uint256 _index) external view returns (address) ``` Returns the role member at `_index` (AccessControlEnumerable). ### getRoleMemberCount(bytes32 \_role) ```solidity function getRoleMemberCount(bytes32 _role) external view returns (uint256) ``` Returns the number of members for `_role`. ### getRoleMembers(bytes32 \_role) ```solidity function getRoleMembers(bytes32 _role) external view returns (address[] memory) ``` Returns all members for `_role` (may be expensive on-chain). ### supportsInterface(bytes4 \_interfaceId) ```solidity function supportsInterface(bytes4 _interfaceId) public view returns (bool) ``` Returns true if the interface is supported (ERC-165). ## Methods ### initialize(...) ```solidity function initialize( address _defaultAdmin, address _nodeOperatorManager, address _nodeOperatorFeeRecipient, uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry ) external ``` Initializes the dashboard with admin roles, fee settings, and confirmation expiry. ### transferVaultOwnership(address \_newOwner) ```solidity function transferVaultOwnership(address _newOwner) external returns (bool) ``` Transfers the vault to a new owner via VaultHub. Requires dual confirmation. Stops fee accrual after transfer. ### voluntaryDisconnect() ```solidity function voluntaryDisconnect() external ``` Disconnects the vault from VaultHub. Collects fees as `feeLeftover` and stops fee accrual. ### recoverFeeLeftover() ```solidity function recoverFeeLeftover() external ``` Recovers previously collected fee leftover to the `feeRecipient` address. ### abandonDashboard(address \_newOwner) ```solidity function abandonDashboard(address _newOwner) external ``` Accepts ownership from VaultHub (after disconnect) and transfers to a new owner. Requires vault to be disconnected. ### reconnectToVaultHub() ```solidity function reconnectToVaultHub() external ``` Accepts ownership and reconnects to VaultHub. Requires `settledGrowth` to be corrected first if fee rate is non-zero. ### connectToVaultHub() ```solidity function connectToVaultHub() public payable ``` Connects the vault to VaultHub. Reverts if `settledGrowth` is not set when fee rate is non-zero. ### connectAndAcceptTier(uint256 \_tierId, uint256 \_requestedShareLimit) ```solidity function connectAndAcceptTier(uint256 _tierId, uint256 _requestedShareLimit) external payable ``` Connects to VaultHub and accepts a tier in one transaction. ### fund() ```solidity function fund() external payable ``` Funds the vault with sent ETH. ### receive() ```solidity receive() external payable ``` Automatically calls `fund()` when fund-on-receive is enabled. ### withdraw(address \_recipient, uint256 \_ether) ```solidity function withdraw(address _recipient, uint256 _ether) external ``` Withdraws ETH from the vault. Limited by `withdrawableValue()`. ### mintShares(address \_recipient, uint256 \_amountOfShares) ```solidity function mintShares(address _recipient, uint256 _amountOfShares) external payable ``` Mints stETH shares. Can include ETH funding in same call. ### mintStETH(address \_recipient, uint256 \_amountOfStETH) ```solidity function mintStETH(address _recipient, uint256 _amountOfStETH) external payable ``` Mints stETH. Reverts if amount is less than 1 share. ### mintWstETH(address \_recipient, uint256 \_amountOfWstETH) ```solidity function mintWstETH(address _recipient, uint256 _amountOfWstETH) external payable ``` Mints wstETH by minting stETH and wrapping it. ### burnShares(uint256 \_amountOfShares) ```solidity function burnShares(uint256 _amountOfShares) external ``` Burns stETH shares. Requires approval. ### burnStETH(uint256 \_amountOfStETH) ```solidity function burnStETH(uint256 _amountOfStETH) external ``` Burns stETH. Requires approval. Reverts if amount is less than 1 share. ### burnWstETH(uint256 \_amountOfWstETH) ```solidity function burnWstETH(uint256 _amountOfWstETH) external ``` Burns wstETH by unwrapping and burning. Requires approval. ### rebalanceVaultWithShares(uint256 \_shares) ```solidity function rebalanceVaultWithShares(uint256 _shares) external ``` Rebalances vault by burning the specified number of shares. ### rebalanceVaultWithEther(uint256 \_ether) ```solidity function rebalanceVaultWithEther(uint256 _ether) external payable ``` Rebalances vault with ETH. Converts to shares internally. ### disburseFee() ```solidity function disburseFee() public ``` Disburses node operator fees permissionlessly. Reverts if fee exceeds abnormally high threshold. ### disburseAbnormallyHighFee() ```solidity function disburseAbnormallyHighFee() external ``` Disburses an abnormally high fee. Requires `DEFAULT_ADMIN_ROLE`. ### setFeeRate(uint256 \_newFeeRate) ```solidity function setFeeRate(uint256 _newFeeRate) external returns (bool) ``` Updates the fee rate. Requires dual confirmation, fresh report, and no corrections after latest report. Disburses outstanding fees before changing rate. ### correctSettledGrowth(int256 \_newSettledGrowth, int256 \_expectedSettledGrowth) ```solidity function correctSettledGrowth(int256 _newSettledGrowth, int256 _expectedSettledGrowth) external returns (bool) ``` Manually corrects the settled growth baseline. Requires dual confirmation. Used to enable fee accrual after reconnection. ### addFeeExemption(uint256 \_exemptedAmount) ```solidity function addFeeExemption(uint256 _exemptedAmount) external ``` Adds a fee exemption to exclude value from fee calculations. Requires `NODE_OPERATOR_FEE_EXEMPT_ROLE`. ### setConfirmExpiry(uint256 \_newConfirmExpiry) ```solidity function setConfirmExpiry(uint256 _newConfirmExpiry) external returns (bool) ``` Sets the confirmation expiry period. Requires dual confirmation. ### setFeeRecipient(address \_newFeeRecipient) ```solidity function setFeeRecipient(address _newFeeRecipient) external ``` Sets the fee recipient address. Requires `NODE_OPERATOR_MANAGER_ROLE`. ### setPDGPolicy(PDGPolicy \_pdgPolicy) ```solidity function setPDGPolicy(PDGPolicy _pdgPolicy) external ``` Sets PDG policy (strict or allow deposits/proofs). Requires `DEFAULT_ADMIN_ROLE`. ### unguaranteedDepositToBeaconChain(IStakingVault.Deposit[] \_deposits) ```solidity function unguaranteedDepositToBeaconChain( IStakingVault.Deposit[] calldata _deposits ) external returns (uint256 totalAmount) ``` Performs direct deposits bypassing PDG. Requires `ALLOW_DEPOSIT_AND_PROVE` policy and `NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE`. Adds fee exemption for deposited amount. ### proveUnknownValidatorsToPDG(IPredepositGuarantee.ValidatorWitness[] \_witnesses) ```solidity function proveUnknownValidatorsToPDG(IPredepositGuarantee.ValidatorWitness[] calldata _witnesses) external ``` Proves validators for PDG after unguaranteed deposit. Requires `ALLOW_PROVE` or `ALLOW_DEPOSIT_AND_PROVE` policy and `NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE`. ### recoverERC20(address \_token, address \_recipient, uint256 \_amount) ```solidity function recoverERC20( address _token, address _recipient, uint256 _amount ) external ``` Recovers ERC-20 tokens or ETH (using EIP-7528 address `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`) from the dashboard. Requires `DEFAULT_ADMIN_ROLE`. ETH recovery excludes `feeLeftover`. ### collectERC20FromVault(address \_token, address \_recipient, uint256 \_amount) ```solidity function collectERC20FromVault( address _token, address _recipient, uint256 _amount ) external ``` Collects ERC-20 tokens from the vault. Requires `COLLECT_VAULT_ERC20_ROLE`. Does not support ETH. ### pauseBeaconChainDeposits() ```solidity function pauseBeaconChainDeposits() external ``` Pauses vault deposits. Requires `PAUSE_BEACON_CHAIN_DEPOSITS_ROLE`. ### resumeBeaconChainDeposits() ```solidity function resumeBeaconChainDeposits() external ``` Resumes vault deposits. Requires `RESUME_BEACON_CHAIN_DEPOSITS_ROLE`. ### requestValidatorExit(bytes \_pubkeys) ```solidity function requestValidatorExit(bytes calldata _pubkeys) external ``` Requests validator exits (voluntary signal to node operators). Requires `REQUEST_VALIDATOR_EXIT_ROLE`. ### triggerValidatorWithdrawals(...) ```solidity function triggerValidatorWithdrawals( bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient ) external payable ``` Triggers validator withdrawals via EIP-7002. Requires `TRIGGER_VALIDATOR_WITHDRAWAL_ROLE` and withdrawal fee via `msg.value`. ### changeTier(uint256 \_tierId, uint256 \_requestedShareLimit) ```solidity function changeTier(uint256 _tierId, uint256 _requestedShareLimit) external returns (bool) ``` Requests a tier change. Requires `VAULT_CONFIGURATION_ROLE` and node operator confirmation via OperatorGrid. ### syncTier() ```solidity function syncTier() external returns (bool) ``` Applies pending tier changes. Requires `VAULT_CONFIGURATION_ROLE` and node operator confirmation. ### updateShareLimit(uint256 \_requestedShareLimit) ```solidity function updateShareLimit(uint256 _requestedShareLimit) external returns (bool) ``` Requests a share limit change. Requires `VAULT_CONFIGURATION_ROLE` and node operator confirmation. ### grantRoles(RoleAssignment[] \_assignments) ```solidity function grantRoles(RoleAssignment[] calldata _assignments) external ``` Mass-grants multiple roles. Each assignment specifies an account and role. ### revokeRoles(RoleAssignment[] \_assignments) ```solidity function revokeRoles(RoleAssignment[] calldata _assignments) external ``` Mass-revokes multiple roles. ### grantRole(bytes32 \_role, address \_account) ```solidity function grantRole(bytes32 _role, address _account) external ``` Grants a role using AccessControl (admin role required). ### revokeRole(bytes32 \_role, address \_account) ```solidity function revokeRole(bytes32 _role, address _account) external ``` Revokes a role using AccessControl (admin role required). ### renounceRole(bytes32 \_role, address \_account) ```solidity function renounceRole(bytes32 _role, address _account) external ``` Renounces a role held by `_account` (caller must be `_account`). ## Related - [StakingVault](/contracts/staking-vault) - [VaultHub](/contracts/vault-hub) - [OperatorGrid](/contracts/operator-grid) - [PredepositGuarantee](/contracts/predeposit-guarantee) - [stVaults Integration Overview](/run-on-lido/stvaults/tech-documentation/integration-overview) --- # DataBus - [Source Code](https://github.com/lidofinance/data-bus/blob/main/contracts/DataBus.sol) :::info The contract is posted at `0x37De961D6bb5865867aDd416be07189D2Dd960e6` and is available in [the test environment](/deployed-contracts/hoodi#data-bus) and [the production environment](/deployed-contracts/#data-bus) ::: ## What is Data Bus? It's a blockchain-based communication channel designed for efficient message exchange between different services using a smart contract on Ethereum. ## Why use Data Bus? Data Bus facilitates the sending of arbitrary events with various data payloads. It offers a minimalistic design, low gas consumption, and requires no active maintenance or support. ## How to use Data Bus? This contract uses a special event called an "abstract event," which is highly customizable and can carry a variety of data types under different event identifiers. It allows for the use of a unified mechanism to handle multiple event types, enhancing flexibility and efficiency in blockchain communication. ### Abstract Event Design The contract defines an event with the following structure: ```solidity event Message( bytes32 indexed eventId, address indexed sender, bytes data ) anonymous; ``` The `anonymous` attribute means the event does not use the standard event signature topic, allowing for more flexible and efficient event handling. For further details on anonymous events, refer to the [Solidity documentation](https://docs.soliditylang.org/en/latest/abi-spec.html#events). ### Emitting Events To emit an event, calculate the hash of your event signature (e.g., `keccak256(bytes('SomeEvent(address,bytes)'))`), which becomes the `eventId`. This identifier, along with the data, is used in the function: ```solidity function sendMessage(bytes32 _eventId, bytes calldata _data) ``` This function logs the event on the blockchain, allowing for any user-defined event to be emitted using the `Message` event template. --- # DepositSecurityModule - [Source Code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/DepositSecurityModule.sol) - [Deployed Contract](https://etherscan.io/address/0xF573E9E3de1f86B085417ab294f56E7920B4e9Be) Due to front-running vulnerability, Lido contributors [proposed](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-5.md) to establish the Deposit Security Committee dedicated to ensuring the safety of deposits on the Beacon chain: - monitoring the history of deposits and the set of Lido keys available for the deposit, signing and disseminating messages allowing deposits; - signing the special message allowing anyone to pause deposits once the malicious Node Operator predeposits are detected. Each member must generate an EOA address to sign messages with their private key. The addresses of the committee members will be added to the smart contract. To make a deposit, we propose to collect a quorum of 4/6 of the signatures of the committee members. Members of the committee can collude with node operators and steal money by signing bad data that contains malicious predeposits. To mitigate this, we propose allowing a single committee member to stop deposits and also enforce space deposits in time (e.g., no more than 150 deposits with 25 blocks in between them) to provide the single honest participant the ability to stop further deposits even if the supermajority colludes. The guardian himself, or anyone else who has a signed pause message, can call `pauseDeposits` that pauses `DepositSecurityModule`. To prevent a replay attack, the guardians sign the block number when malicious predeposits are observed. After a certain number of blocks (`pauseIntentValidityPeriodBlocks`) message becomes invalid. Values of the parameters `maxDepositsPerBlock` and `minDepositBlockDistance` are controlled by Lido DAO and must be harmonized with `appearedEthAmountPerDayLimit` of [`OracleReportSanityChecker`](/contracts/oracle-report-sanity-checker). These parameters are set in the StakingRouter contract independently for each module. ## View Methods ### getOwner() Returns the contract's owner address. ```solidity function getOwner() external view returns (address); ``` ### getPauseIntentValidityPeriodBlocks() Returns `pauseIntentValidityPeriodBlocks` (see `pauseDeposits`). ```solidity function getPauseIntentValidityPeriodBlocks() external view returns (uint256); ``` ### getMaxOperatorsPerUnvetting() Returns the maximum number of operators per unvetting (see `unvetSigningKeys`). ```solidity function getMaxOperatorsPerUnvetting() external view returns (uint256); ``` ### getGuardianQuorum() Returns the number of valid guardian signatures required to vet (depositRoot, nonce) pair. ```solidity function getGuardianQuorum() external view returns (uint256); ``` ### getGuardians() Returns guardian committee member list. ```solidity function getGuardians() external view returns (address[] memory); ``` ### isGuardian() Checks whether the given address is a guardian. ```solidity function isGuardian(address addr) external view returns (bool); ``` #### Parameters | Name | Type | Description | | ------ | --------- | ------------------- | | `addr` | `address` | Valid ETH-1 address | ### getGuardianIndex() Returns index of the guardian, or -1 if the address is not a guardian. ```solidity function getGuardianIndex(address addr) external view returns (int256); ``` #### Parameters | Name | Type | Description | | ------ | --------- | ------------------- | | `addr` | `address` | Valid ETH-1 address | ### getLastDepositBlock() Returns the block number of the last deposit made through the module. ```solidity function getLastDepositBlock() external view returns (uint256); ``` ### isMinDepositDistancePassed() Returns whether the deposit distance is greater than the minimum required for the staking module with id `stakingModuleId`. ```solidity function isMinDepositDistancePassed(uint256 stakingModuleId) external view returns (bool); ``` #### Parameters | Name | Type | Description | | ----------------- | --------- | ------------------------ | | `stakingModuleId` | `uint256` | Id of the staking module | :::note The distance is reset when a deposit is made to any module. This prevents a front-run attack by colluding guardians on several modules at once, providing the necessary window for an honest guardian to react and pause deposits to all modules. ::: ### isDepositsPaused() Returns whether deposits are paused. ```solidity function isDepositsPaused() external view returns (bool); ``` ## Methods ### setOwner() Sets new owner. ```solidity function setOwner(address newValue) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; - `newValue` is zero address. ::: #### Parameters | Name | Type | Description | | ---------- | --------- | ----------------- | | `newValue` | `address` | New owner address | ### setPauseIntentValidityPeriodBlocks() Sets `pauseIntentValidityPeriodBlocks`. ```solidity function setPauseIntentValidityPeriodBlocks(uint256 newValue) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; - `newValue` is 0 (zero). ::: #### Parameters | Name | Type | Description | | ---------- | --------- | ---------------------------------------------------- | | `newValue` | `uint256` | Number of blocks after which message becomes invalid | ### setMaxOperatorsPerUnvetting() Sets `maxOperatorsPerUnvetting`. ```solidity function setMaxOperatorsPerUnvetting(uint256 newValue) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; - `newValue` is 0 (zero). ::: #### Parameters | Name | Type | Description | | ---------- | --------- | ----------------------------------------------- | | `newValue` | `uint256` | New maximum number of operators per unvetting | ### setGuardianQuorum() Sets the number of valid guardian signatures required to vet (depositRoot, nonce) pair (aka "quorum"). ```solidity function setGuardianQuorum(uint256 newValue) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; ::: #### Parameters | Name | Type | Description | | ---------- | --------- | ---------------- | | `newValue` | `uint256` | New quorum value | ### addGuardian() Adds a guardian address and sets a new quorum value. ```solidity function addGuardian(address addr, uint256 newQuorum) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; - `addr` is zero address; - `addr` is already a guardian. ::: #### Parameters | Name | Type | Description | | ----------- | --------- | ---------------- | | `addr` | `address` | Guardian address | | `newQuorum` | `uint256` | New Quorum value | ### addGuardians() Adds a set of guardian addresses and sets a new quorum value. ```solidity function addGuardians(address[] memory addresses, uint256 newQuorum) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; - any of the `addresses` is zero address; - any of the `addresses` is already a guardian. ::: #### Parameters | Name | Type | Description | | ----------- | ----------- | --------------------------- | | `addresses` | `address[]` | Array of Guardian addresses | | `newQuorum` | `uint256` | New Quorum value | ### removeGuardian() Removes a guardian with the given address and sets a new quorum value. ```solidity function removeGuardian(address addr, uint256 newQuorum) external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner; - `addr` is not a guardian. ::: #### Parameters | Name | Type | Description | | ----------- | --------- | ---------------- | | `addr` | `address` | Guardian address | | `newQuorum` | `uint256` | New Quorum value | ### pauseDeposits() Pauses deposits if both conditions are satisfied (reverts otherwise): 1. The function is called by a guardian OR `sig` is a valid signature by a guardian of the data defined below. 2. `block.number - blockNumber <= pauseIntentValidityPeriodBlocks` The signature, if present, must be produced for keccak256 hash of the following message (each component taking 32 bytes): | PAUSE_MESSAGE_PREFIX | blockNumber | Does nothing if deposits are already paused. In case of an emergency, the function `pauseDeposits` is supposed to be called by all guardians. Thus, only the first call will do the actual change. So the other calls would be OK operations from the point of view of the protocol logic. ```solidity function pauseDeposits(uint256 blockNumber, Signature memory sig) external; ``` #### Parameters | Name | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------------------------------------ | | `blockNumber` | `uint256` | Block number with malicious predeposits have been observed by the guardian | | `sig` | `Signature` | Short ECDSA guardian signature as defined in [EIP-2098](https://eips.ethereum.org/EIPS/eip-2098) | ### unpauseDeposits() Unpauses deposits. ```solidity function unpauseDeposits() external; ``` :::note Reverts if any of the following is true: - `msg.sender` is not the owner. - Deposits not paused. ::: ### depositBufferedEther() Verifies that all deposit security conditions are satisfied, then calls [`StakingRouter.deposit`](/contracts/staking-router#deposit), which pulls the required ETH from [`Lido`](/contracts/lido#withdrawdepositableether) and performs the deposits. Reverts if any of the required conditions are not met. :::note Reverts if any of the following is true: 1. onchain deposit root is different from the provided one; 2. onchain module nonce is different from the provided one; 3. quorum is zero or the number of guardian signatures is less than the quorum; 4. min deposit distance is not passed; 5. `blockHash` is zero or not equal to `blockhash(blockNumber)`; 6. deposits are paused; 7. an invalid or non-guardian signature received; 8. signatures are not sorted in ascending order by the guardian address. 9. any downstream contract call reverts. See `StakingRouter.deposit` for details. ::: Signatures must be sorted in ascending order by the address of the guardian. Each signature must be produced for the keccak256 hash of the following message (each component taking 32 bytes): | ATTEST_MESSAGE_PREFIX | blockNumber | blockHash | depositRoot | stakingModuleId | nonce | ```solidity function depositBufferedEther( uint256 blockNumber, bytes32 blockHash, bytes32 depositRoot, uint256 stakingModuleId, uint256 nonce, Signature[] calldata sortedGuardianSignatures ) external; ``` #### Parameters | Name | Type | Description | | -------------------------- | ------------- | -------------------------------------------------------------------------------------------------- | | `blockNumber` | `uint256` | Number of the current deposit block | | `blockHash` | `bytes32` | Hash of the current deposit block | | `depositRoot` | `bytes32` | Deposit root of the Ethereum DepositContract | | `stakingModuleId` | `uint256` | Id of the staking module to deposit with | | `nonce` | `uint256` | Nonce of key operations of the staking module | | `sortedGuardianSignatures` | `Signature[]` | Short ECDSA guardians signatures as defined in [EIP-2098](https://eips.ethereum.org/EIPS/eip-2098) | ### unvetSigningKeys() Unvets signing keys for the given node operators. :::note Reverts if any of the following is true: 1. The nonce is not equal to the on-chain nonce of the staking module; 2. nodeOperatorIds is not packed with 8 bytes per id; 3. vettedSigningKeysCounts is not packed with 16 bytes per count; 4. the number of node operators is greater than maxOperatorsPerUnvetting; 5. the signature is invalid or the signer is not a guardian; 6. blockHash is zero or not equal to the blockhash(blockNumber). ::: The signature, if present, must be produced for the keccak256 hash of the following message: | UNVET_MESSAGE_PREFIX | blockNumber | blockHash | stakingModuleId | nonce | nodeOperatorIds | vettedSigningKeysCounts | ```solidity function unvetSigningKeys( uint256 blockNumber, bytes32 blockHash, uint256 stakingModuleId, uint256 nonce, bytes calldata nodeOperatorIds, bytes calldata vettedSigningKeysCounts, Signature calldata sig ) external; ``` #### Parameters | Name | Type | Description | | ------------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | `blockNumber` | `uint256` | Number of the current deposit block | | `blockHash` | `bytes32` | Hash of the current deposit block | | `stakingModuleId` | `uint256` | Id of the staking module to deposit with | | `nonce` | `uint256` | Nonce of key operations of the staking module | | `nodeOperatorIds` | `bytes` | The list of node operator IDs packed with 8 bytes per id | | `vettedSigningKeysCounts` | `bytes` | The list of vetted signing keys counts packed with 16 bytes per count | | `sig` | `Signature` | Short ECDSA guardians signatures as defined in [EIP-2098](https://eips.ethereum.org/EIPS/eip-2098) | --- # EIP712StETH - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/EIP712StETH.sol) - [Deployed contract](https://etherscan.io/address/0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7) `EIP712StETH` serves as a dedicated helper contract for `stETH`, crucial for the complete support of [ERC-2612 compliant signed approvals](https://eips.ethereum.org/EIPS/eip-2612). ## Why This Helper Is Needed The original [`Lido/StETH`](/contracts/lido) contract is implemented in Solidity `0.4.24`, while this helper is implemented in Solidity `0.8.9`. The newer compiler version enables access to the current network's chain id via the globally available variable [`block.chainid`](https://docs.soliditylang.org/en/v0.8.9/units-and-global-variables.html#block-and-transaction-properties). The chain id is mandatory for signature inclusion as per [EIP-155](https://eips.ethereum.org/EIPS/eip-155) to prevent replay attacks, wherein an attacker intercepts a valid network transmission and then rebroadcasts it on another network fork. Consequently, `EIP-155` compliance is critical for securing [`ERC-2612`](https://eips.ethereum.org/EIPS/eip-2612) signed approvals. ## View Methods ### domainSeparatorV4() This method returns the `EIP712`-compatible hashed [domain separator](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), which is valid for `stETH` token permit signatures. The domain separator is essential in preventing a signature intended for one dApp from functioning in another (thereby averting a signature collision in a broader sense). ```sol function domainSeparatorV4(address _stETH) returns (bytes32) ``` Also, consider the [`eip712Domain()`](/contracts/eip712-steth#eip712domain) method that can construct a domain separator from `StETH`-specific fields on the client's side, such as within a dApp or a wallet. For instance, Metamask relies on [`eth_signTypedData_v4`](https://docs.metamask.io/wallet/how-to/sign-data/#use-eth_signtypeddata_v4), which requires a non-hashed domain separator being provided. ### hashTypedDataV4() This method returns the hash of a fully encoded `EIP712`-compatible message for this domain. The method can validate the input data against the provided `v, r, s` secp256k1 components. ```sol function hashTypedDataV4(address _stETH, bytes32 _structHash) returns (bytes32) ``` #### Parameters | Name | Type | Description | | ------------- | --------- | ------------------------------------- | | `_stETH` | `address` | Address of the deployed `stETH` token | | `_structHash` | `bytes32` | Hash of the data structure | For a specific use case, see the [StETHPermit.permit()](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.4.24/StETHPermit.sol#L99-L112) implementation. ### eip712Domain() This method returns the fields and values necessary to construct a domain separator on the client's side. The method resembles the one proposed in [ERC-5267](https://eips.ethereum.org/EIPS/eip-5267), with the only difference being that it doesn't return unused fields. ```sol function eip712Domain(address _stETH) returns ( string memory name, string memory version, uint256 chainId, address verifyingContract ) ``` #### Parameters | Name | Type | Description | | -------- | --------- | ------------------------------------- | | `_stETH` | `address` | Address of the deployed `stETH` token | #### Returns | Name | Type | Description | | ------------------- | --------- | ----------------------------- | | `name` | `string` | Name of the token | | `version` | `string` | Version of the token | | `chainId` | `uint256` | Chain identifier | | `verifyingContract` | `address` | Address of the token contract | :::note Provided the correct `_stETH` [deployed](/deployed-contracts) address, it returns: - ("Liquid staked Ether 2.0", "2", 1, 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) for Mainnet. - ("Liquid staked Ether 2.0", "2", 560048, 0x3508A952176b3c15387C97BE809eaffB1982176a) for Hoodi. ::: This method facilitates domain separator construction on the client's side, such as in a wallet or widget: ```js function makeDomainSeparator(name, version, chainId, verifyingContract) { return web3.utils.keccak256( web3.eth.abi.encodeParameters( ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], [ web3.utils.keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), web3.utils.keccak256(name), web3.utils.keccak256(version), chainId, verifyingContract, ], ), ) } ``` ## Useful External Links - [The Magic of Digital Signatures on Ethereum](https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7) - [ERC-2612: The Ultimate Guide to Gasless ERC-20 Approvals](https://medium.com/frak-defi/erc-2612-the-ultimate-guide-to-gasless-erc-20-approvals-2cd32ddee534) - [Metamask sign-data](https://docs.metamask.io/wallet/how-to/sign-data/#use-eth_signtypeddata_v4) --- # HashConsensus - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/oracle/HashConsensus.sol) - [Deployed instance for AccountingOracle](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288) - [Deployed instance for ValidatorsExitBusOracle](https://etherscan.io/address/0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a) - [Deployed instance for CSFeeOracle](https://etherscan.io/address/0x71093efF8D8599b5fA340D665Ad60fA7C80688e4) :::info It's advised to read [What is Lido Oracle mechanism](/guides/oracle-operator-manual#intro) before ::: ## What is HashConsensus HashConsensus is a contract responsible for managing oracle members committee and allowing the members to reach consensus on a data hash for each reporting frame. Time is divided in frames of equal length, each having reference slot and processing deadline. Report data must be gathered by looking at the world state (both Ethereum Consensus and Execution Layers) at the moment of the frameโ€™s reference slot (including any state changes made in that slot), and must be processed before the frameโ€™s processing deadline. Frame length is defined in Ethereum Consensus Layer epochs. Reference slot for each frame is set to the last slot of the epoch preceding the frameโ€™s first epoch. The processing deadline is set to the last slot of the last epoch of the frame. Note that all state changes a report processing could entail are guaranteed to be observed while gathering data for the next frameโ€™s report. This is an essential property given that oracle reports sometimes have to contain diffs instead of the entire state, which might be impractical or even impossible to transmit and process. Consensus members rotate within one time into two subsets: - Non-fast-lane members - [Fast-lane members](/contracts/hash-consensus#fast-lane-members) Once the consensus is gathered, a [Report processor](#report-processor-ireportasyncprocessor) would allow submitting and processing the actual report data. The latter is a part of the [phased Oracle report flow](/docs/guides/oracle-operator-manual.md#oracle-phases). ## Report processor (`IReportAsyncProcessor`) `IReportAsyncProcessor` defines the interface for a contract that gets consensus reports (i.e. hashes) pushed to and processes them asynchronously. `HashConsensus` doesn't expect any specific behavior from a report processor, and guarantees the following: 1. `HashConsensus` won't submit reports via `IReportAsyncProcessor.submitConsensusReport` or ask to discard reports via `IReportAsyncProcessor.discardConsensusReport` for any slot up to (and including) the slot returned from `IReportAsyncProcessor.getLastProcessingRefSlot`. 2. `HashConsensus` won't accept member reports (and thus won't include such reports in calculating the consensus) that have `consensusVersion` argument of the `HashConsensus.submitReport` call holding a diff. value than the one returned from `IReportAsyncProcessor.getConsensusVersion` at the moment of the `HashConsensus.submitReport` call. There are two core protocol contracts that implements this interface: - [AccountingOracle](/contracts/accounting-oracle) - [ValidatorsExitBusOracle](/contracts/validators-exit-bus-oracle) ## Fast-lane members Fast lane members is a subset of all members that changes each reporting frame. These members can, and are expected to, submit a report during the first part of the frame called the "fast lane interval" and defined via [setFrameConfig](#setframeconfig) or [setFastLaneLengthSlots](#setfastlanelengthslots). The calculation of the Fast-lane members subset depends on `frameIndex`, `totalMembers` and `quorum`. Under regular circumstances, all other members are only allowed to submit a report after the fast lane interval passes. This is done to encourage each oracle from the full set to participate in reporting on a regular basis, and identify any malfunctioning members. The fast lane subset consists of quorum members; selection is implemented as a sliding window of the quorum width over member indices (`mod` total members). The window advances by one index each reporting frame. With the fast lane mechanism active, it's sufficient for the monitoring to check that consensus is consistently reached during the fast lane part of each frame to conclude that all members are active and share the same consensus rules. :::note There is no guarantee that, at any given time, it holds true that only the current fast lane members can or were able to report during the currently-configured fast lane interval of the current frame. In particular, this assumption can be violated in any frame during which the members set, initial epoch, or the quorum number was changed, or the fast lane interval length was increased. Therefore, the fast lane mechanism should not be used for any purpose other than monitoring of the members liveness, and monitoring tools should take into consideration the potential irregularities within frames with any configuration changes. ::: ## View methods ### getChainConfig() Returns the immutable chain parameters required to calculate epoch and slot given a timestamp. ```solidity function getChainConfig() external view returns ( uint256 slotsPerEpoch, uint256 secondsPerSlot, uint256 genesisTime ) ``` #### Returns | Name | Type | Description | | ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | | `slotsPerEpoch` | `uint256` | Number of slots per epoch, `32` by default | | `secondsPerSlot` | `uint256` | The time allocated for each slot, `12` by default | | `genesisTime` | `uint256` | Consensus Layer genesis time, `1606824023` on [Mainnet](https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21) | ### getFrameConfig() Returns the time-related configuration. ```solidity function getFrameConfig() external view returns ( uint256 initialEpoch, uint256 epochsPerFrame, uint256 fastLaneLengthSlots ) ``` #### Returns | Name | Type | Description | | --------------------- | --------- | -------------------------------------------------------------------- | | `initialEpoch` | `uint256` | Epoch of the frame with zero index | | `epochsPerFrame` | `uint256` | Length of a frame in epochs | | `fastLaneLengthSlots` | `uint256` | Length of the fast lane interval in slots; see `getIsFastLaneMember` | ### getCurrentFrame() Returns the current reporting frame. ```solidity function getCurrentFrame() external view returns ( uint256 refSlot, uint256 reportProcessingDeadlineSlot ) ``` #### Returns | Name | Type | Description | | ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `refSlot` | `uint256` | The frame's reference slot: if the data the consensus is being reached upon includes or depends on any onchain state, this state should be queried at the reference slot. If the slot contains a block, the state should include all changes from that block. | | `reportProcessingDeadlineSlot` | `uint256` | The last slot at which the report can be processed by the report processor contract. | ### getInitialRefSlot() Returns the earliest possible reference slot, i.e. the reference slot of the reporting frame with zero index. ```solidity function getInitialRefSlot() external view returns (uint256) ``` ### getIsMember() Returns whether the given address is currently a member of the consensus. ```solidity function getIsMember(address addr) external view returns (bool) ``` ### getIsFastLaneMember() Returns whether the given address is a fast lane member for the current reporting frame. ```solidity function getIsFastLaneMember(address addr) external view returns (bool) ``` ### getMembers() Returns all current members, together with the last reference slot each member submitted a report for. ```solidity function getMembers() external view returns ( address[] memory addresses, uint256[] memory lastReportedRefSlots ) ``` ### getFastLaneMembers() Returns the subset of the oracle committee members (consisting of `quorum` items) that changes each frame. ```solidity function getFastLaneMembers() external view returns ( address[] memory addresses, uint256[] memory lastReportedRefSlots ) ``` ### getQuorum() Returns quorum number ```solidity function getQuorum() external view returns (uint256) ``` ### getReportProcessor() Returns report processor address, i.e oracle address ```solidity function getReportProcessor() external view returns (address) ``` ### getConsensusState() Returns info about the current frame and consensus state in that frame. ```solidity function getConsensusState() external view returns ( uint256 refSlot, bytes32 consensusReport, bool isReportProcessing ) ``` #### Returns Returns info about the current frame and consensus state in that frame. | Name | Type | Description | | -------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- | | `refSlot` | `uint256` | Reference slot of the current reporting frame. | | `consensusReport` | `bytes32` | Consensus report for the current frame, if any. Zero bytes otherwise. | | `isReportProcessing` | `bool` | If consensus report for the current frame is already being processed. Consensus can be changed before the processing starts. | ### getReportVariants() Returns report variants and their support for the current reference slot. ```solidity function getReportVariants() external view returns ( bytes32[] memory variants, uint256[] memory support ) ``` ### getConsensusStateForMember() Returns the extended information related to an oracle committee member with the given address and the current consensus state. Provides all the information needed for an oracle daemon to decide if it needs to submit a report. ```solidity function getConsensusStateForMember(address addr) external view returns (MemberConsensusState memory result) ``` #### Parameters | Name | Type | Description | | ------ | --------- | ------------------- | | `addr` | `address` | The member address. | #### Returns Returns a new type `MemberConsensusState` ```solidity struct MemberConsensusState { /// @notice Current frame's reference slot. uint256 currentFrameRefSlot; /// @notice Consensus report for the current frame, if any. Zero bytes otherwise. bytes32 currentFrameConsensusReport; /// @notice Whether the provided address is a member of the oracle committee. bool isMember; /// @notice Whether the oracle committee member is in the fast lane members subset /// of the current reporting frame. See `getIsFastLaneMember`. bool isFastLane; /// @notice Whether the oracle committee member is allowed to submit a report at /// the moment of the call. bool canReport; /// @notice The last reference slot for which the member submitted a report. uint256 lastMemberReportRefSlot; /// @notice The hash reported by the member for the current frame, if any. /// Zero bytes otherwise. bytes32 currentFrameMemberReport; } ``` ## Methods ### updateInitialEpoch() Sets a new initial epoch given that the current initial epoch is in the future. Can only be called by users with `DEFAULT_ADMIN_ROLE`. ```solidity function updateInitialEpoch(uint256 initialEpoch) external ``` - Reverts with `InitialEpochAlreadyArrived()` if current epoch more or equal initial epoch from current frame config. - Reverts with `InitialEpochRefSlotCannotBeEarlierThanProcessingSlot()` if initial frame refSlot less than last processing refSlot. - Reverts with `EpochsPerFrameCannotBeZero()` if `epochsPerFrame` from frame config is zero. - Reverts with `FastLanePeriodCannotBeLongerThanFrame()` if `fastLaneLengthSlots` from config more than frame length. ### setFrameConfig() Updates the time-related configuration. Can only be called by users with `MANAGE_FRAME_CONFIG_ROLE`. ```solidity function setFrameConfig(uint256 epochsPerFrame, uint256 fastLaneLengthSlots) external ``` - Reverts with `EpochsPerFrameCannotBeZero()` if `epochsPerFrame` is zero. - Reverts with `FastLanePeriodCannotBeLongerThanFrame()` if `fastLaneLengthSlots` more than frame length. #### Parameters | Name | Type | Description | | --------------------- | --------- | --------------------------------------------------------------------- | | `epochsPerFrame` | `uint256` | ALength of a frame in epochs. | | `fastLaneLengthSlots` | `uint256` | Length of the fast lane interval in slots; see `getIsFastLaneMember`. | ### setFastLaneLengthSlots() Sets the duration of the fast lane interval of the reporting frame. Can only be called by users with `MANAGE_FAST_LANE_CONFIG_ROLE`. ```solidity function setFastLaneLengthSlots(uint256 fastLaneLengthSlots) external ``` - Reverts with `FastLanePeriodCannotBeLongerThanFrame()` if `fastLaneLengthSlots` more than frame length. #### Parameters | Name | Type | Description | | --------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fastLaneLengthSlots` | `uint256` | The length of the fast lane reporting interval in slots. Setting it to zero disables the fast lane subset, allowing any oracle to report starting from the first slot of a frame and until the frame's reporting deadline. | ### addMember() Add a new member of the consensus. Can only be called by users with `DISABLE_CONSENSUS_ROLE` role if `quorum` set as UINT256_MAX. Can only be called by users with `MANAGE_MEMBERS_AND_QUORUM_ROLE` role if `quorum` not set as UINT256_MAX. ```solidity function addMember(address addr, uint256 quorum) external ``` - Reverts with `DuplicateMember()` if `addr` address is already the member of consensus. - Reverts with `AddressCannotBeZero()` if `addr` address is zero. - Reverts with `QuorumTooSmall(uint256 minQuorum, uint256 receivedQuorum)` if `quorum` less or equal than total members of consensus divided by 2 (`quorum <= total members / 2`) ### removeMember() Remove a member from the consensus. Can only be called by users with `DISABLE_CONSENSUS_ROLE` role if `quorum` set as UINT256_MAX. Can only be called by users with `MANAGE_MEMBERS_AND_QUORUM_ROLE` role if `quorum` not set as UINT256_MAX. ```solidity function removeMember(address addr, uint256 quorum) external ``` - Reverts with `NonMember()` if `addr` address doesn't exists - Reverts with `QuorumTooSmall(uint256 minQuorum, uint256 receivedQuorum)` if `quorum` less or equal than total members of consensus divided by 2 (`quorum <= total members / 2`) ### setQuorum() Update consensus quorum Can only be called by users with `DISABLE_CONSENSUS_ROLE` role if `quorum` set as UINT256_MAX. Can only be called by users with `MANAGE_MEMBERS_AND_QUORUM_ROLE` role if `quorum` not set as UINT256_MAX. ```solidity function setQuorum(uint256 quorum) external ``` - Reverts with `QuorumTooSmall(uint256 minQuorum, uint256 receivedQuorum)` if `quorum` less or equal than total members of consensus divided by 2 (`quorum <= total members / 2`) ### disableConsensus() Disable consensus quorum, i.e set quorum as `UINT256_MAX` (UNREACHABLE_QUORUM) Can only be called by users with `DISABLE_CONSENSUS_ROLE` ```solidity function disableConsensus() external ``` - Reverts with `QuorumTooSmall(uint256 minQuorum, uint256 receivedQuorum)` if `quorum` less or equal than total members of consensus divided by 2 (`quorum <= total members / 2`) ### setReportProcessor() Set report processor address, i.e oracle address Can only be called by users with `MANAGE_REPORT_PROCESSOR_ROLE`. ```solidity function setReportProcessor(address newProcessor) external ``` - Reverts with `ReportProcessorCannotBeZero()` if `newProcessor` address is zero. - Reverts with `NewProcessorCannotBeTheSame()` if `newProcessor` address is equal to the previous processor address. ### submitReport() Used by oracle members to submit hash of the data calculated for the given reference slot. ```solidity function submitReport(uint256 slot, bytes32 report, uint256 consensusVersion) external ``` #### Parameters | Name | Type | Description | | ------------------ | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `slot` | `uint256` | The reference slot the data was calculated for. Reverts if doesn't match the current reference slot. | | `report` | `bytes32` | Hash of the data calculated for the given reference slot. | | `consensusVersion` | `uint256` | Version of the oracle consensus rules. Reverts if doesn't match the version returned by the currently set consensus report processor, or zero if no report processor is set. | #### Reverts - Reverts with `InvalidSlot()` if `slot` is zero. - Reverts with `InvalidSlot()` if `slot` is not equal current frame refSlot. - Reverts with `NumericOverflow()` if `slot` is more than `UINT64_MAX` - Reverts with `EmptyReport()` if `reports` is zero hash (`bytes32(0)`) - Reverts with `NonMember()` if caller address doesn't exists in members array - Reverts with `UnexpectedConsensusVersion(uint256 expected, uint256 received)` if `consensusVersion` is not equal report processor consensus version. - Reverts with `StaleReport()` if the current frame slot is more than the frame report processing deadline slot. - Reverts with `NonFastLaneMemberCannotReportWithinFastLaneInterval()` if the current frame slot is less or equal frame ref slot plus fastlane length AND the member who submits the report is not fastlane member. - Reverts with `ConsensusReportAlreadyProcessing()` if the member sends a report for the same slot. - Reverts with `DuplicateReport()` if the member already sends the report. ## Events ### FrameConfigSet() Emits when a new frame config set via [`setFrameConfig`](#setframeconfig). ```solidity event FrameConfigSet(uint256 newInitialEpoch, uint256 newEpochsPerFrame) ``` ### FastLaneConfigSet() Emits when fast lane length changed (i.e., length defined in slots). ```solidity event FastLaneConfigSet(uint256 fastLaneLengthSlots) ``` ### MemberAdded() Emits when a new member of consensus is added. ```solidity event MemberAdded(address indexed addr, uint256 newTotalMembers, uint256 newQuorum) ``` ### MemberRemoved() Emits when an existing member of consensus is removed. ```solidity event MemberRemoved(address indexed addr, uint256 newTotalMembers, uint256 newQuorum) ``` ### QuorumSet() Emits when a quorum of consensus members is changed. ```solidity event QuorumSet(uint256 newQuorum, uint256 totalMembers, uint256 prevQuorum) ``` ### ReportReceived() Emits when a new report received for the provided `refSlot` by `member` containing the `report` hash. ```solidity event ReportReceived(uint256 indexed refSlot, address indexed member, bytes32 report) ``` ### ConsensusReached() Emits when a consensus reached for the provided `refSlot` containing the `report` hash. ```solidity event ConsensusReached(uint256 indexed refSlot, bytes32 report, uint256 support) ``` ### ConsensusLost() Emits when the previously established consensus for the provided `refSlot` is disbanded. ```solidity event ConsensusLost(uint256 indexed refSlot) ``` ### ReportProcessorSet() Emits when the report processor is changed from `prevProcessor` to `processor`. Both addresses must comply with the `IReportAsyncProcessor` interface. ```solidity event ReportProcessorSet(address indexed processor, address indexed prevProcessor) ``` --- # LazyOracle - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/LazyOracle.sol) - [Deployed contract](https://etherscan.io/address/0x5DB427080200c235F2Ae8Cd17A7be87921f7AD6c) Oracle adapter for stVaults. Stores per-vault reports, applies sanity checks, and forwards vault updates to VaultHub. ## What is LazyOracle? LazyOracle is a lightweight oracle for stVaults: - stores the latest report metadata (timestamp, ref slot, tree root, CID) - validates vault proofs against a report tree root - applies per-vault accounting updates to VaultHub - quarantines vaults with suspicious value deltas It is called **lazy** because it stores only the report root and metadata each round; per-vault data is expanded on-demand via Merkle proofs only when a vault operation needs it. ## How it works 1. `AccountingOracle` publishes a report root and metadata via `updateReportData()`. 2. Anyone can submit per-vault updates with Merkle proofs via `updateVaultData()`. 3. LazyOracle validates proofs and checks reward/fee bounds. 4. Sanity-checked data is forwarded to VaultHub via `applyVaultReport()`. Per-vault report submissions are **permissionless**: any account can call `updateVaultData` with a valid Merkle proof from the latest report root. ### Report freshness A vault report freshness is determined by VaultHub based on the report timestamp stored in LazyOracle. When stale, the vault cannot perform operations like withdrawals, mints, beacon chain deposits or disconnect. ### Quarantine mechanics LazyOracle applies a quarantine buffer for sudden total value jumps that cannot be verified immediately via `inOutDelta`. Value increases beyond the expected reward threshold are quarantined for a configurable period before being released to VaultHub. ``` Time 0: Total Value = 100 ETH โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 100 ETH Active โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Time 1: Sudden jump of +50 ETH โ†’ start quarantine for 50 ETH โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 100 ETH Active โ”‚ โ”‚ 50 ETH Quarantined โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Time 2: Another jump of +70 ETH โ†’ wait for current quarantine to expire โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 100 ETH Active โ”‚ โ”‚ 50 ETH Quarantined โ”‚ โ”‚ 70 ETH Quarantine Queue โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Time 3: First quarantine expires โ†’ add 50 ETH to active value, start new quarantine for 70 ETH โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 150 ETH Active โ”‚ โ”‚ 70 ETH Quarantined โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Time 4: Second quarantine expires โ†’ add 70 ETH to active value โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 220 ETH Active โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### Quarantine state machine ``` States: โ€ข NO_QUARANTINE: No active quarantine, all value is immediately available โ€ข QUARANTINE_ACTIVE: Total value increase is quarantined, waiting for expiration โ€ข QUARANTINE_EXPIRED: Quarantine period passed, quarantined value can be released โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ NO_QUARANTINE โ”‚ reported > threshold โ”‚QUARANTINE_ACTIVE โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ โ”‚ โ”‚ quarantined=0 โ”‚ โ”‚ quarantined>0 โ”‚ โ”‚ startTime=0 โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค startTime>0 โ”‚ โ”‚ | โ”‚ time quarantined + rewards โ”‚ time โ‰ฅ โ”‚ โ”‚ (release old, start new) โ”‚ quarantine period โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ reported โ‰ค threshold OR โ”‚ QUARANTINE_EXPIRED โ”‚ โ”‚ increase โ‰ค quarantined + rewards โ”‚ โ”‚ โ”‚ โ”‚ quarantined>0 โ”‚ โ”‚ โ”‚ startTime>0 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค time>=expiration โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Legend: โ€ข threshold = onchainTotalValue * (100% + maxRewardRatio) โ€ข increase = reportedTotalValue - onchainTotalValue โ€ข quarantined = total value increase that is currently quarantined โ€ข rewards = expected EL/CL rewards based on maxRewardRatio โ€ข expiration = quarantine.startTimestamp + quarantinePeriod ``` Normal top-ups via `fund()` do not go through quarantine since they can be verified on-chain via `inOutDelta`. Only consolidations or deposits that bypass the vault's balance are quarantined. ## Constants | Constant | Value | Description | | ------------------------------ | ------------------------ | --------------------------------- | | `MAX_QUARANTINE_PERIOD` | 30 days | Maximum allowed quarantine period | | `MAX_REWARD_RATIO` | 65535 (type(uint16).max) | Maximum reward ratio (~655%) | | `MAX_LIDO_FEE_RATE_PER_SECOND` | 10 ether | Maximum Lido fee rate per second | ## Structs ### QuarantineInfo ```solidity struct QuarantineInfo { bool isActive; // Whether quarantine is active uint256 pendingTotalValueIncrease; // Amount quarantined uint256 startTimestamp; // When quarantine started uint256 endTimestamp; // When quarantine expires uint256 totalValueRemainder; // Additional value waiting in queue } ``` ### VaultInfo Aggregated vault information returned by view methods: ```solidity struct VaultInfo { address vault; // Vault address uint256 aggregatedBalance; // availableBalance + stagedBalance int256 inOutDelta; // Current in/out delta bytes32 withdrawalCredentials; // Vault withdrawal credentials uint256 liabilityShares; // Current liability shares uint256 maxLiabilityShares; // Maximum liability shares uint256 mintableStETH; // Remaining mintable stETH uint96 shareLimit; // Share limit from connection uint16 reserveRatioBP; // Reserve ratio in basis points uint16 forcedRebalanceThresholdBP; // Forced rebalance threshold uint16 infraFeeBP; // Infrastructure fee uint16 liquidityFeeBP; // Liquidity fee uint16 reservationFeeBP; // Reservation fee bool pendingDisconnect; // Whether vault is pending disconnect } ``` ## View methods ### latestReportData() ```solidity function latestReportData() external view returns ( uint256 timestamp, uint256 refSlot, bytes32 treeRoot, string memory reportCid ) ``` Returns latest report metadata. ### latestReportTimestamp() ```solidity function latestReportTimestamp() external view returns (uint256) ``` Returns latest report timestamp. ### quarantinePeriod() ```solidity function quarantinePeriod() external view returns (uint256) ``` Returns quarantine period duration in seconds. ### maxRewardRatioBP() ```solidity function maxRewardRatioBP() external view returns (uint256) ``` Returns max reward ratio in basis points. Used to determine quarantine threshold. ### maxLidoFeeRatePerSecond() ```solidity function maxLidoFeeRatePerSecond() external view returns (uint256) ``` Returns max Lido fee rate per second in wei. ### quarantineValue(address \_vault) ```solidity function quarantineValue(address _vault) external view returns (uint256) ``` Returns total value pending in quarantine for a vault (includes both `pendingTotalValueIncrease` and `totalValueRemainder`). ### vaultQuarantine(address \_vault) ```solidity function vaultQuarantine(address _vault) external view returns (QuarantineInfo memory) ``` Returns detailed quarantine info for a vault. Returns zeroed struct if no active quarantine. ### vaultsCount() ```solidity function vaultsCount() external view returns (uint256) ``` Returns number of vaults connected to VaultHub. ### batchVaultsInfo(uint256 \_offset, uint256 \_limit) ```solidity function batchVaultsInfo(uint256 _offset, uint256 _limit) external view returns (VaultInfo[] memory) ``` Returns vault info for a range of vaults. Offset is 0-indexed from VaultHub vault list. ### vaultInfo(address \_vault) ```solidity function vaultInfo(address _vault) external view returns (VaultInfo memory) ``` Returns aggregated info for a specific vault. ### batchValidatorStatuses(bytes[] \_pubkeys) ```solidity function batchValidatorStatuses(bytes[] calldata _pubkeys) external view returns (IPredepositGuarantee.ValidatorStatus[] memory batch) ``` Returns validator statuses from PredepositGuarantee for multiple pubkeys. ## Methods ### initialize(address \_admin, uint256 \_quarantinePeriod, uint256 \_maxRewardRatioBP, uint256 \_maxLidoFeeRatePerSecond) ```solidity function initialize( address _admin, uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond ) external initializer ``` Initializes LazyOracle with admin and sanity parameters. ### updateSanityParams(...) ```solidity function updateSanityParams( uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond ) external ``` Updates sanity bounds. Requires `UPDATE_SANITY_PARAMS_ROLE`. ### updateReportData(...) ```solidity function updateReportData( uint256 _vaultsDataTimestamp, uint256 _vaultsDataRefSlot, bytes32 _vaultsDataTreeRoot, string memory _vaultsDataReportCid ) external ``` Publishes the report root and metadata. Only callable by `AccountingOracle`. ### updateVaultData(...) ```solidity function updateVaultData( address _vault, uint256 _totalValue, uint256 _cumulativeLidoFees, uint256 _liabilityShares, uint256 _maxLiabilityShares, uint256 _slashingReserve, bytes32[] calldata _proof ) external ``` Applies a per-vault update with a Merkle proof. Permissionless - anyone can call with valid proof. **Sanity checks performed:** 1. Report must be newer than vault's previous report 2. Total value increase is quarantined if above reward threshold 3. Dynamic total value calculation must not underflow 4. Cumulative Lido fees must be monotonically increasing 5. Cumulative Lido fees increase must not exceed max rate 6. `maxLiabilityShares` must be >= `liabilityShares` and <= on-chain value ### removeVaultQuarantine(address \_vault) ```solidity function removeVaultQuarantine(address _vault) external ``` Removes quarantine for a vault. Only callable by `VaultHub`. ## Permissions | Role | Description | | --------------------------- | ------------------------------------------------------------------------ | | `DEFAULT_ADMIN_ROLE` | Admin role for granting/revoking other roles | | `UPDATE_SANITY_PARAMS_ROLE` | Can update sanity parameters (quarantine period, reward ratio, fee rate) | ## Related - [VaultHub](/contracts/vault-hub) - [AccountingOracle](/contracts/accounting-oracle) - [OperatorGrid](/contracts/operator-grid) - [PredepositGuarantee](/contracts/predeposit-guarantee) - [stVaults Technical Design](/run-on-lido/stvaults/tech-documentation/tech-design) --- # Lido - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.4.24/Lido.sol) - [Deployed contract](https://etherscan.io/address/0xae7ab96520de3a18e5e111b5eaab095312d7fe84) Liquid staking pool and a related [ERC-20](https://eips.ethereum.org/EIPS/eip-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 - allocating buffered ether between the unfinalized withdrawal requests ([WithdrawalQueueERC721](/contracts/withdrawal-queue-erc721)), and validator deposits pulled by [StakingRouter](/contracts/staking-router) Also, Lido is an [ERC-20](https://eips.ethereum.org/EIPS/eip-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](https://eips.ethereum.org/EIPS/eip-2612) permit and [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) signature validation extensions. Other contracts are bound to the core and have the following responsibilities: - [`LidoLocator`](/contracts/lido-locator): protocol-wide address book which contains references to all meaningful parts of the Lido protocol on-chain - [`WithdrawalQueueERC721`](/contracts/withdrawal-queue-erc721): a withdrawal requests FIFO queue and a respective NFT (unstETH) - [`StakingRouter`](/contracts/staking-router): hub, which manages staking modules and distributes the stake among them - [`NodeOperatorsRegistry`](/contracts/node-operators-registry): original module, responsible for managing the curated set of node operators - [`OracleReportSanityChecker`](/contracts/oracle-report-sanity-checker): helper for validation of oracle report parameters and smoothening token rebases - [`Burner`](/contracts/burner): vault to contain `stETH` that ought to be burned on oracle report - [`WithdrawalVault`](/contracts/withdrawal-vault): vault to collect partial and full withdrawals coming from the Beacon Chain - [`LidoExecutionLayerRewardsVault`](/contracts/lido-execution-layer-rewards-vault): vault to collect priority fees and MEV rewards coming from validators of the pool - [`DepositSecurityModule`](/contracts/deposit-security-module): protection from deposit frontrunning vulnerability - [`AccountingOracle`](/contracts/accounting-oracle): oracle committee, which gathers an accounting report for the protocol - [`Accounting`](/contracts/accounting): applies oracle reports and distributes rewards - [`VaultHub`](/contracts/vault-hub): stVaults coordination and external share accounting - [`PredepositGuarantee`](/contracts/predeposit-guarantee): predeposit protection for stVaults - [`EIP712StETH`](/contracts/eip712-steth): 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 is later used to fulfill withdrawal requests via [`WithdrawalQueueERC721`](/contracts/withdrawal-queue-erc721) or pulled by [`StakingRouter`](/contracts/staking-router) to be deposited as a validator stake. To withdraw the underlying ETH back, a user may use the [`WithdrawalQueueERC721`](/contracts/withdrawal-queue-erc721) 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 deposited as a validator stake. Deposits follow the pull model: [`StakingRouter`](/contracts/staking-router) pulls the required amount of ether from the buffer via [`withdrawDepositableEther()`](/contracts/lido#withdrawdepositableether) when it executes initial 32 ETH deposits (guarded by [`DepositSecurityModule`](/contracts/deposit-security-module) to prevent the deposit frontrunning vulnerability) or top-ups of `0x02`-type validators (initiated via [`TopUpGateway`](/contracts/top-up-gateway)). ```mermaid graph LR; A[/ \]--deposit-->DSM[DepositSecurityModule]-->SR[StakingRouter]; B[/ \]--topUp-->TUG[TopUpGateway]-->SR; SR--withdrawDepositableEther-->Lido--ether-->SR; SR-->DC[DepositContract]; ``` ### Deposits reserve Buffered ether is split into three priority-ordered buckets: 1. **Deposits reserve** โ€” buffer portion available for CL deposits, protected from withdrawals demand. Filled first. 2. **Withdrawals reserve** โ€” covers unfinalized withdrawal requests from the remaining buffer. 3. **Unreserved** โ€” excess buffer available for additional CL deposits beyond the reserve. ```txt โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Total Buffered Ether โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ”‚โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—โ—‹โ—‹โ—‹โ—‹โ—‹โ”‚โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ—‹โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ””โ”€ Deposits Reserve โ”€โ”ผโ”€ Withdrawals Reserve โ”€โ”˜ โ”œโ”€ Unreserved โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€ Unfinalized stETH โ”€โ”€โ”€โ”€โ”€โ”˜ โ— โ€” covered by Buffered Ether โ—‹ โ€” not covered by Buffered Ether ``` ```js depositsReserve = min(totalBuffered, storedDepositsReserve) withdrawalsReserve = min(totalBuffered - depositsReserve, unfinalizedStETH) unreserved = totalBuffered - depositsReserve - withdrawalsReserve depositableEther = depositsReserve + unreserved ``` The deposits reserve guarantees ether availability for stake rebalancing between staking modules and migration deposits, regardless of withdrawal demand. The reserve target is configured via [`setDepositsReserveTarget()`](/contracts/lido#setdepositsreservetarget); the effective reserve is consumed as deposits are performed and is restored to the target on each oracle report. ## Redeem The token might be redeemed for ether through the protocol using the [`WithdrawalQueueERC721`](/contracts/withdrawal-queue-erc721) contract leveraging [staking withdrawals](https://ethereum.org/en/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](https://ethereum.org/en/upgrades/merge/) Ethereum upgrade) or fulfilled withdrawal requests (starting from [Lido V2](https://blog.lido.fi/introducing-lido-v2/)). A rebase happens when the oracle report is applied. 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: ```js 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` - the total amount of ether controlled by the protocol, a sum of: - buffered balance - ether stored on the contract and hasn't been deposited or locked for withdrawals yet - CL validators balance - the total balance of Lido validators active on the Consensus Layer. This value is reported by oracles and makes the strongest impact on stETH total supply change - CL pending balance - the total balance of Lido-attributed deposits waiting in the Consensus Layer pending deposits queue, reported by oracles - deposited since report balance - ether sent to the official Deposit contract after the last oracle report and thus not yet reflected in the reported CL balances - external ether - the amount of ether backing external (stVaults) shares, accounted separately via [`VaultHub`](/contracts/vault-hub) ### External shares (stVaults) Lido V3 introduces **external shares**, which represent stETH minted against stVault collateral. These shares are tracked separately from core pool shares to keep the core pool solvent while allowing overcollateralized minting. - `getExternalShares()` returns total external shares outstanding. - `getExternalEther()` returns the ETH amount backing external shares. - `getMaxExternalRatioBP()` caps external shares as a ratio of total shares. Accounting uses internal (core pool) ether/shares for rebase calculations and applies external share accounting separately through `VaultHub`. For example, assume that we have: ```js totalShares = 5 totalPooledEther = 10 ETH sharesOf(Alice) -> 1 sharesOf(Bob) -> 4 ``` Therefore: ```js 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. ```js 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. - Consensus Layer stats: - the total balance of Lido validators active on the Consensus Layer - the total balance of Lido-attributed deposits pending in the Consensus Layer deposit queue - per-staking-module validator balances (used for rewards distribution between modules) - 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: AccountingOracle submits the report to the `Accounting` contract, which simulates and applies the report, then calls into Lido to update pooled ether and rebase observers. 1. Memorize the pre-state that will be required for incremental updates of the protocol balance 2. Validate the report data using [`OracleReportSanityChecker`](/contracts/oracle-report-sanity-checker) 3. Calculate the amount of ether to be locked on [`WithdrawalQueueERC721`](/contracts/withdrawal-queue-erc721) and move the respective amount of shares to be burnt to [`Burner`](/contracts/burner) 4. Using [`OracleReportSanityChecker`](/contracts/oracle-report-sanity-checker) calculate the amounts of ether that can be withdrawn from [`LidoExecutionLayerRewardsVault`](/contracts/lido-execution-layer-rewards-vault) and [`WithdrawalVault`](/contracts/withdrawal-vault) as well as the number of shares that can be burnt from [`Burner`](/contracts/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`](/contracts/withdrawal-queue-erc721) 6. Burn the previously requested shares from [`Burner`](/contracts/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 ```mermaid graph LR; S[/Users' stake/]:::orange-.->B[Lido Buffer]; W[WithdrawalsVault]-->B; E[LidoExecutionLayerRewardsVault]-->B; B-->D[StakingRouter]; B-->WQ[WithdrawalQueue]; classDef orange fill:#f96; ``` 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 balances are updated according to the report - the deposits reserve is restored to its configured target - rewards are distributed among stakers, staking modules and protocol treasury ## Standards Contract implements the following Ethereum standards: - [ERC-20: Token Standard](https://eips.ethereum.org/EIPS/eip-20) - [ERC-2612: Permit Extension for ERC-20 Signed Approvals](https://eips.ethereum.org/EIPS/eip-2612) - [EIP-712: Typed structured data hashing and signing](https://eips.ethereum.org/EIPS/eip-712) - [ERC-1271: Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271) ## Staking-related Methods ### fallback Sends funds to the pool and mints `StETH` tokens to the `msg.sender` address ```sol 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 [Lido Rewards-Share Program](https://research.lido.fi/t/rewards-share-program-2024/6812) for referral program details. ```sol function submit(address _referral) payable returns (uint256) ``` | Parameter | Type | Description | | ----------- | --------- | ------------------------- | | `_referral` | `address` | Optional referral address | Returns the number of `StETH` shares generated. ### getBufferedEther() Returns the amount of ether temporarily buffered on the contract's balance. ```sol 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](https://ethereum.org/en/staking/deposit-contract/) or [`WithdrawalsQueueERC721`](/contracts/withdrawal-queue-erc721) ::: ### isStakingPaused() Returns staking state: whether it's paused or not. ```sol function isStakingPaused() view returns (bool) ``` :::note 'staking' here means the ability to accept new [submit](/contracts/lido#submit) requests ::: ### getCurrentStakeLimit() Returns how much ether can be staked in the current block. ```sol 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. ```sol function getStakeLimitFullInfo() view returns ( bool isStakingPaused, bool isStakingLimitSet, uint256 currentStakeLimit, uint256 maxStakeLimit, uint256 maxStakeLimitGrowthBlocks, uint256 prevStakeLimit, uint256 prevStakeBlockNumber ) ``` | Name | Type | Description | | --------------------------- | --------- | ----------------------------------------------------------------------- | | `isStakingPaused` | `bool` | Staking pause state (equivalent to return of `isStakingPaused()`) | | `isStakingLimitSet` | `bool` | Whether the stake limit is set or not | | `currentStakeLimit` | `uint256` | Current stake limit (equivalent to return of `getCurrentStakeLimit()`) | | `maxStakeLimit` | `uint256` | Max stake limit | | `maxStakeLimitGrowthBlocks` | `uint256` | Blocks needed to restore max stake limit from the fully exhausted state | | `prevStakeLimit` | `uint256` | Previously reached stake limit | | `prevStakeBlockNumber` | `uint256` | Previously seen block number | ## Deposit-related methods ### withdrawDepositableEther() Withdraws `_amount` of buffered ether to [`StakingRouter`](/contracts/staking-router) to be deposited to the Consensus Layer (the pull model of deposits). Can be called only by the [`StakingRouter`](/contracts/staking-router) contract. ```sol function withdrawDepositableEther(uint256 _amount, uint256 _seedDepositsCount) ``` | Parameter | Type | Description | | -------------------- | --------- | --------------------------------------------------------------------------- | | `_amount` | `uint256` | Amount of ether to withdraw | | `_seedDepositsCount` | `uint256` | Number of initial (seed) 32 ETH deposits performed; `0` in case of a top-up | :::note Reverts if depositing is not allowed at the moment (see [`canDeposit()`](/contracts/lido#candeposit)), if `_amount` is zero, or if `_amount` exceeds the currently depositable ether. The withdrawn amount is forwarded to `StakingRouter.receiveDepositableEther()` within the same call, and the stored deposits reserve is decreased accordingly. `_seedDepositsCount` increments the legacy deposited validators counter kept for backward compatibility (see [`getBeaconStat()`](/contracts/lido#getbeaconstat)). ::: ### getDepositableEther() Returns the amount of ether available to deposit. ```sol function getDepositableEther() view returns (uint256) ``` :::note Equals the buffered ether minus the withdrawals reserve, i.e., the sum of the deposits reserve and the unreserved buffer. See [Deposits reserve](/contracts/lido#deposits-reserve). ::: ### canDeposit() Returns `true` if depositing buffered ether to the consensus layer is allowed. ```sol function canDeposit() view returns (bool) ``` :::note Depositing is allowed when the protocol is not stopped and bunker mode is not active. ::: ### getDepositsReserve() Returns the currently effective deposits reserve โ€” the buffer portion available for CL deposits, protected from withdrawals demand. ```sol function getDepositsReserve() view returns (uint256) ``` :::note Capped by the current buffered ether. Consumed by [`withdrawDepositableEther()`](/contracts/lido#withdrawdepositableether) and restored to the configured target on each oracle report. ::: ### getWithdrawalsReserve() Returns the currently effective withdrawals reserve โ€” the buffer portion allocated to unfinalized withdrawal requests. Computed after the deposits reserve is applied. ```sol function getWithdrawalsReserve() view returns (uint256) ``` ### getDepositsReserveTarget() Returns the configured target that the deposits reserve is restored to on each oracle report. ```sol function getDepositsReserveTarget() view returns (uint256) ``` ## Accounting-related methods Accounting and report application are handled by the [`Accounting`](/contracts/accounting) contract, which is called by `AccountingOracle` and updates Lido state during the report cycle. ### getTotalPooledEther() Returns the entire amount of ether controlled by the protocol ```sol function getTotalPooledEther() view returns (uint256) ``` :::note The sum of all ETH balances in the protocol, equals to the total supply of `stETH`. ::: ### getExternalEther() Returns the amount of ether backing external shares (stVaults). ```sol function getExternalEther() view returns (uint256) ``` ### getExternalShares() Returns total external shares minted against stVault collateral. ```sol function getExternalShares() view returns (uint256) ``` ### getMaxExternalRatioBP() Returns the maximum ratio of external shares to total shares (basis points). ```sol function getMaxExternalRatioBP() view returns (uint256) ``` ### getBalanceStats() Returns the full balance model data: CL balances from the last oracle report and deposit counters. ```sol function getBalanceStats() view returns ( uint256 clValidatorsBalanceAtLastReport, uint256 clPendingBalanceAtLastReport, uint256 depositedSinceLastReport, uint256 depositedForCurrentReport ) ``` | Name | Type | Description | | --------------------------------- | --------- | -------------------------------------------------------------------------------------------------- | | `clValidatorsBalanceAtLastReport` | `uint256` | Sum of Lido validators' active balances on the Consensus Layer at the last oracle report (wei) | | `clPendingBalanceAtLastReport` | `uint256` | Sum of Lido-attributed pending deposits on the Consensus Layer at the last oracle report (wei) | | `depositedSinceLastReport` | `uint256` | Ether deposited since the last oracle report reference slot (wei) | | `depositedForCurrentReport` | `uint256` | Ether deposited between the last report reference slot and the current frame's reference slot (wei) | ### getTotalELRewardsCollected() Returns the total amount of execution layer rewards collected to Lido contract buffer. ```sol function getTotalELRewardsCollected() view returns (uint256) ``` ### getBeaconStat() Returns the tuple of key statistics related to the Consensus Layer. ```sol function getBeaconStat() view returns ( uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance ) ``` | Name | Type | Description | | --------------------- | --------- | ------------------------------------------------------------------------------------------ | | `depositedValidators` | `uint256` | Number of initial (seed) 32 ETH deposits ever performed | | `beaconValidators` | `uint256` | Always equals `depositedValidators`, kept for compatibility | | `beaconBalance` | `uint256` | Sum of the CL validators balance and the CL pending balance at the last oracle report | :::warning DEPRECATED: `beaconValidators` does not reflect the actual Consensus Layer state and always equals `depositedValidators`. Use [`getBalanceStats()`](/contracts/lido#getbalancestats) for new integrations. ::: ### receiveELRewards() A payable function for execution layer rewards. Can be called only by the [`LidoExecutionLayerRewardsVault`](/contracts/lido-execution-layer-rewards-vault) contract. ```sol function receiveELRewards() payable ``` ### receiveWithdrawals() A payable function for withdrawals acquisition. Can be called only by [`WithdrawalVault`](/contracts/withdrawal-vault). ```sol function receiveWithdrawals() payable ``` ## Protocol levers ### stop() Stop pool routine operations. Can be called only by the bearer of `PAUSE_ROLE` ```sol function stop() ``` ### resume() Resume pool routine operations. Can be called only by the bearer of `RESUME_ROLE` ```sol function resume() ``` ### pauseStaking() Stops accepting new ether to the protocol. Can be called only by the bearer of `STAKING_PAUSE_ROLE` ```sol 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` ```sol function resumeStaking() ``` :::note Staking could be rate-limited by imposing a limit on the stake amount at each moment in time, see [`setStakingLimit()`](/contracts/lido#setstakinglimit) and [`removeStakingLimit()`](/contracts/lido#removestakinglimit). ::: ### setStakingLimit() Sets the staking rate limit. Can be called only by the bearer of `STAKING_CONTROL_ROLE` Limit explanation scheme: ```txt * โ–ฒ Stake limit * โ”‚..... ..... ........ ... .... ... Stake limit = max * โ”‚ . . . . . . . . . * โ”‚ . . . . . . . . . * โ”‚ . . . . . * โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> Time * โ”‚ ^ ^ ^ ^^^ ^ ^ ^ ^^^ ^ Stake events ``` ```sol function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) ``` | Parameter | Type | Description | | ----------------------------- | --------- | ------------------------------------- | | `_maxStakeLimit` | `uint256` | Max stake limit value | | `_stakeLimitIncreasePerBlock` | `uint256` | Stake 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` ```sol function removeStakingLimit() ``` ### setMaxExternalRatioBP() Sets the maximum ratio of external shares to total shares (basis points). Can be called only by the bearer of `STAKING_CONTROL_ROLE` ```sol function setMaxExternalRatioBP(uint256 _maxExternalRatioBP) ``` | Parameter | Type | Description | | --------------------- | --------- | --------------------------------------------------- | | `_maxExternalRatioBP` | `uint256` | Max external shares ratio in basis points (0-10000) | ### setDepositsReserveTarget() Sets the deposits reserve target โ€” the amount of buffered ether to protect for CL deposits between oracle reports. See [Deposits reserve](/contracts/lido#deposits-reserve). Can be called only by the bearer of `BUFFER_RESERVE_MANAGER_ROLE` ```sol function setDepositsReserveTarget(uint256 _newDepositsReserveTarget) ``` | Parameter | Type | Description | | --------------------------- | --------- | ---------------------------------- | | `_newDepositsReserveTarget` | `uint256` | New deposits reserve target in wei | :::note If the target is lowered below the current effective reserve, the reserve is reduced immediately. Increases are not applied mid-frame and take effect on the next oracle report processing. ::: ## `ERC-20`-related Methods ### name() Returns the name of the token. ```sol function name() view returns (string) ``` :::note Always returns `Liquid staked Ether 2.0`. ::: ### symbol() Returns the symbol of the token. ```sol function symbol() view returns (string) ``` :::note Always returns `stETH`. ::: ### decimals() Returns the number of decimals for getting user representation of a token amount. ```sol function decimals() view returns (uint8) ``` :::note Always returns `18`. ::: ### totalSupply() Returns the number of tokens in existence. ```sol 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` ```sol 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`](/contracts/lido#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. ```sol function allowance(address _owner, address _spender) view returns (uint256) ``` | Parameter | Type | Description | | ---------- | --------- | ------------------ | | `_owner` | `address` | Address of owner | | `_spender` | `address` | Address 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 ```sol function approve(address _spender, uint256 _amount) returns (bool) ``` | Parameter | Type | Description | | ---------- | --------- | ------------------ | | `_spender` | `address` | Address of spender | | `_amount` | `uint256` | Amount 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](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol#L42). ```sol function increaseAllowance(address _spender, uint256 _addedValue) returns (bool) ``` | Parameter | Type | Description | | ------------- | --------- | -------------------------------------- | | `_sender` | `address` | Address of spender | | `_addedValue` | `uint256` | Amount 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](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol#L42). ```sol function decreaseAllowance(address _spender, uint256 _subtractedValue) returns (bool) ``` | Parameter | Type | Description | | ------------------ | --------- | -------------------------------------- | | `_sender` | `address` | Address of spender | | `_subtractedValue` | `uint256` | Amount 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. ```sol function transfer(address _recipient, uint256 _amount) returns (bool) ``` | Parameter | Type | Description | | ------------ | --------- | ---------------------------- | | `_recipient` | `address` | Address of tokens recipient | | `_amount` | `uint256` | Amount 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. ```sol function transferFrom( address _sender, address _recipient, uint256 _amount ) returns (bool) ``` | Parameter | Type | Description | | ------------ | --------- | -------------------- | | `_sender` | `address` | Address of spender | | `_recipient` | `address` | Address of recipient | | `_amount` | `uint256` | Amount 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. ::: ## Shares-related Methods ### getTotalShares() Returns the total amount of shares in existence. ```sol function getTotalShares() view returns (uint256) ``` ### sharesOf() Returns the number of shares owned by `_account` ```sol function sharesOf(address _account) view returns (uint256) ``` ### getSharesByPooledEth() Returns the number of shares that corresponds to `_ethAmount` of the protocol-controlled ether. ```sol function getSharesByPooledEth(uint256 _ethAmount) view returns (uint256) ``` ### getPooledEthByShares() Returns the amount of ether that corresponds to `_sharesAmount` token shares. ```sol function getPooledEthByShares(uint256 _sharesAmount) view returns (uint256) ``` ### transferShares() Moves token shares from the caller's account to the provided recipient account. ```sol function transferShares(address _recipient, uint256 _sharesAmount) returns (uint256) ``` | Parameter | Type | Description | | --------------- | --------- | ---------------------------- | | `_recipient` | `address` | Address of shares recipient | | `_sharesAmount` | `uint256` | Amount 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. ```sol function transferSharesFrom( address _sender, address _recipient, uint256 _sharesAmount ) returns (uint256) ``` | Parameter | Type | Description | | --------------- | --------- | -------------------- | | `_sender` | `address` | Address of spender | | `_recipient` | `address` | Address of recipient | | `_sharesAmount` | `uint256` | Amount 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. ::: ## `ERC-2612`-related methods ### nonces() Returns the current nonce for the `owner`. This value must be included whenever a signature is generated for an ERC-2612 permit. ```sol 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. ```sol 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. ```sol function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) ``` | Parameter | Type | Description | | ---------- | --------- | ------------------------------ | | `owner` | `address` | Address of spender | | `spender` | `address` | Address of recipient | | `value` | `uint256` | Allowance to set | | `deadline` | `uint256` | Permit expiry time | | `v` | `uint8` | extended `secp256k1` signature | | `r` | `bytes32` | extended `secp256k1` signature | | `s` | `bytes32` | extended `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`). ::: ## `ERC-712`-related methods ### eip712Domain() Returns the fields and values that describe the domain separator used by this contract for EIP-712 ```sol function eip712Domain() view returns ( string name, string version, uint256 chainId, address verifyingContract ) ``` ## General Methods ### getLidoLocator() Returns the address of [LidoLocator](/contracts/lido-locator). ```sol function getLidoLocator() view returns (address) ``` ### getContractVersion() Returns the current contract version. ```sol function getContractVersion() view returns (uint256) ``` :::note Always returns `4`. ::: ### transferToVault() Overrides default AragonApp behavior to disallow recovery. ```sol function transferToVault(address _token) ``` | Parameter | Type | Description | | --------- | --------- | ---------------------------------- | | `_token` | `address` | Token to be sent to recovery vault | :::note Always reverts with `NOT_SUPPORTED` reason ::: --- # LidoExecutionLayerRewardsVault - [Source Code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/LidoExecutionLayerRewardsVault.sol) - [Deployed Contract](https://etherscan.io/address/0x388C818CA8B9251b393131C08a736A67ccB19297) A vault for temporary storage of execution layer (EL) rewards (MEV and tx priority fee). See the Lido improvement proposal [#12](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-12.md). Both the transaction priority fee and MEV rewards are collected by specifying the contract's address as the coinbase (`feeRecipient`). Additionally, MEV rewards are also extracted whenever payload builders include an explicit transaction that transfers MEV shares to the `feeRecipient` in the payload. Thereby, the contract features a payable receive function that accepts incoming ether. Only the [`Lido`](lido) contract can withdraw the accumulated rewards to distribute them between `stETH` holders as part of the [`Accounting Oracle`](accounting-oracle) report. NB: Any ether sent to the contract by accident is unrecoverable and will be distributed by the protocol as accrued rewards. ## Methods ### receive() Allows the contract to receive ETH via transactions. Emits the `ETHReceived` event. ```sol receive() external payable; ``` ### withdrawRewards() Move all accumulated EL rewards to the Lido contract. Can only be called by the Lido contract. Returns the ether amount withdrawn. ```sol function withdrawRewards(uint256 _maxAmount) external returns (uint256 amount) ``` #### Parameters: | Name | Type | Description | | ------------ | --------- | ----------------------------- | | `_maxAmount` | `uint256` | Max amount of ETH to withdraw | ### recoverERC20() Transfers the given amount of the ERC20-token (defined by the provided token contract address) currently belonging to the vault contract address to the Lido treasury address. Emits the `ERC20Recovered` event. ```sol function recoverERC20(address _token, uint256 _amount) external ``` #### Parameters: | Name | Type | Description | | --------- | --------- | ----------------------- | | `_token` | `address` | ERC20-compatible token | | `_amount` | `uint256` | token amount to recover | ### recoverERC721() Transfers the given tokenId of the ERC721-compatible NFT (defined by the provided token contract address) currently belonging to the vault contract address to the Lido treasury address. Emits the `ERC721Recovered` event. ```sol function recoverERC721(address _token, uint256 _tokenId) external ``` #### Parameters: | Name | Type | Description | | ---------- | --------- | ----------------------- | | `_token` | `address` | ERC721-compatible token | | `_tokenId` | `uint256` | minted token id | --- # LidoLocator - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/LidoLocator.sol) - [Deployed contract](https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb) LidoLocator is the universal address book for the Lido protocol. It follows the well-known [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) pattern. ## Upgradability The contract uses [OssifiableProxy](/contracts/ossifiable-proxy) for upgradability and does not use storage for the address book. Instead, all addresses are embedded into the implementation's bytecode as immutables for gas efficiency, allowing one to update them along with a proxy implementation. ## Methods ### accountingOracle() Returns an address of the [AccountingOracle contract](/contracts/accounting-oracle) ```sol function accountingOracle() view returns(address); ``` ### accounting() Returns an address of the [Accounting contract](/contracts/accounting). ```sol function accounting() view returns(address); ``` ### depositSecurityModule() Returns an address of the [DepositSecurityModule contract](/contracts/deposit-security-module) ```sol function depositSecurityModule() view returns(address); ``` ### elRewardsVault() Returns an address of the [LidoExecutionLayerRewardsVault contract](/contracts/lido-execution-layer-rewards-vault) ```sol function elRewardsVault() view returns(address); ``` ### lido() Returns an address of the [Lido contract](/contracts/lido) ```sol function lido() external view returns(address); ``` ### oracleReportSanityChecker() Returns an address of the [OracleReportSanityChecker contract](/contracts/oracle-report-sanity-checker) ```sol function oracleReportSanityChecker() view returns(address); ``` ### burner() Returns an address of the [Burner contract](/contracts/burner) ```sol function burner() view returns(address); ``` ### stakingRouter() Returns an address of the [StakingRouter contract](/contracts/staking-router) ```sol function stakingRouter() view returns(address); ``` ### treasury() Returns an address of the treasury ```sol function treasury() view returns(address); ``` ### validatorsExitBusOracle() Returns an address of the [ValidatorsExitBusOracle contract](/contracts/validators-exit-bus-oracle) ```sol function validatorsExitBusOracle() external view returns(address); ``` ### withdrawalQueue() Returns an address of the [WithdrawalQueueERC721 contract](/contracts/withdrawal-queue-erc721) ```sol function withdrawalQueue() view returns(address); ``` ### withdrawalVault() Returns an address of the [WithdrawalVault contract](/contracts/withdrawal-vault) ```sol function withdrawalVault() view returns(address); ``` ### postTokenRebaseReceiver() Returns an address of the contract following the `IPostTokenRebaseReceiver` interface described inside `Lido`. ```sol function postTokenRebaseReceiver() view returns(address); ``` ### oracleDaemonConfig() Returns an address of the [OracleDaemonConfig contract](/contracts/oracle-daemon-config) ```sol function oracleDaemonConfig() view returns(address); ``` ### triggerableWithdrawalsGateway() Returns an address of the [TriggerableWithdrawalsGateway contract](/contracts/triggerable-withdrawals-gateway) ```sol function triggerableWithdrawalsGateway() view returns(address); ``` ### consolidationGateway() Returns an address of the [ConsolidationGateway contract](/contracts/consolidation-gateway) ```sol function consolidationGateway() view returns(address); ``` ### topUpGateway() Returns an address of the [TopUpGateway contract](/contracts/top-up-gateway) ```sol function topUpGateway() view returns(address); ``` ### validatorExitDelayVerifier() Returns an address of the [ValidatorExitDelayVerifier contract](/contracts/validator-exit-delay-verifier) ```sol function validatorExitDelayVerifier() view returns(address); ``` ### predepositGuarantee() Returns an address of the [PredepositGuarantee contract](/contracts/predeposit-guarantee) ```sol function predepositGuarantee() view returns(address); ``` ### wstETH() Returns an address of the [wstETH contract](/contracts/wsteth) ```sol function wstETH() view returns(address); ``` ### vaultHub() Returns an address of the [VaultHub contract](/contracts/vault-hub) ```sol function vaultHub() view returns(address); ``` ### vaultFactory() Returns an address of the [VaultFactory contract](/contracts/staking-vault-factory) ```sol function vaultFactory() view returns(address); ``` ### lazyOracle() Returns an address of the [LazyOracle contract](/contracts/lazy-oracle) ```sol function lazyOracle() view returns(address); ``` ### operatorGrid() Returns an address of the [OperatorGrid contract](/contracts/operator-grid) ```sol function operatorGrid() view returns(address); ``` ### coreComponents() Returns a batch of core components addresses at once. It's just a more gas-efficient way of calling several public getters at once. ```sol function coreComponents() view returns( address elRewardsVault, address oracleReportSanityChecker, address stakingRouter, address treasury, address withdrawalQueue, address withdrawalVault ); ``` ### oracleReportComponents() Returns a batch of addresses that is used specifically during oracle report handling in the Lido contract. It's just a more gas-efficient way of calling several public getters at once. ```sol function oracleReportComponents() view returns( address accountingOracle, address oracleReportSanityChecker, address burner, address withdrawalQueue, address postTokenRebaseReceiver, address stakingRouter, address vaultHub ); ``` --- # MevBoostRelayAllowedList - [Source Code](https://github.com/lidofinance/mev-boost-relay-allowed-list/blob/main/contracts/MEVBoostRelayAllowedList.vy) - [Deployed Contract (mainnet)](https://etherscan.io/address/0xf95f069f9ad107938f6ba802a3da87892298610e) - [Deployed Contract (holeลกky)](https://holesky.etherscan.io/address/0x2d86C5855581194a386941806E38cA119E50aEA3) MEV-Boost relay allowed list is a simple contract storing a list of relays that have been approved by DAO for use in [MEV-Boost](https://github.com/flashbots/mev-boost) setups. The data from the contract is used to generate a configuration file that contains a list of relays that should be connected to by the Node Operators participating in Lido. ## View methods ### get_owner() Retrieves the current contract owner. ```vyper @view @external def get_owner() -> address ``` ### get_manager() Retrieves the current manager entity (returns zero address if no entity is assigned). ```vyper @view @external def get_manager() -> address ``` ### get_relays_amount() Retrieves the current total amount of allowed relays. ```vyper @view @external def get_relays_amount() -> uint256 ``` ### get_relays() Retrieves all of the currently allowed relays. ```vyper @view @external def get_relays() -> DynArray[Relay, MAX_NUM_RELAYS] ``` ### get_relay_by_uri() Retrieves the relay with the provided uri. ```vyper @view @external def get_relay_by_uri(relay_uri: String[MAX_STRING_LENGTH]) -> bool ``` #### Parameters: | Name | Type | Description | |-------------|-----------------------------|------------------| | `relay_uri` | `String[MAX_STRING_LENGTH]` | URI of the relay | :::note Reverts if no relay found. ::: ### get_allowed_list_version() Retrieves the current version of the allowed relays list. ```vyper @view @external def get_allowed_list_version() -> uint256: ``` ## Methods ### add_relay() Appends relay to the allowed list. Bumps the allowed list version. ```vyper @external def add_relay( uri: String[MAX_STRING_LENGTH], operator: String[MAX_STRING_LENGTH], is_mandatory: bool, description: String[MAX_STRING_LENGTH] ) ``` #### Parameters: | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `uri` | `String[MAX_STRING_LENGTH]` | URI of the relay | | `operator` | `String[MAX_STRING_LENGTH]` | Name of the relay operator | | `is_mandatory` | `bool` | If the relay is mandatory for usage for Lido Node Operator | | `description` | `String[MAX_STRING_LENGTH]` | Description of the relay in free format | :::note Reverts if any of the following is true: - called by anyone except the owner or manager - relay with provided `uri` already allowed - `uri` is empty ::: ### remove_relay() Removes the previously allowed relay from the set. Bumps the allowed list version. ```vyper @external def remove_relay(uri: String[MAX_STRING_LENGTH]): ``` #### Parameters: | Name | Type | Description | |--------|-----------------------------|------------------| | `uri` | `String[MAX_STRING_LENGTH]` | URI of the relay | :::note Reverts if any of the following is true: - called by anyone except the owner or manager - if relay with provided `uri` is not allowed - `uri` is empty ::: ### change_owner() Change current owner to the new one. ```vyper @external def change_owner(owner: address) ``` #### Parameters: | Name | Type | Description | |---------|-----------|--------------------------| | `owner` | `address` | Address of the new owner | :::note Reverts if any of the following is true: - called by anyone except the current owner - `owner` is the current owner - `owner` is zero address ::: ### set_manager() Sets `manager` as the current management entity. ```vyper @external def set_manager(manager: address) ``` #### Parameters: | Name | Type | Description | |---------|---------|----------------------------| | manager | address | Address of the new manager | :::note Reverts if any of the following is true: - called by anyone except the current owner - `manager` is equal to the previously set value - `manager` is zero address ::: ### dismiss_manager() Dismisses the current management entity. ```vyper @external def dismiss_manager() ``` :::note Reverts if any of the following is true: - called by anyone except the current owner - no `manager` was set previously ::: ### recover_erc20() Transfers ERC20 tokens from the contract's balance to the `recipient`. ```vyper @external def recover_erc20(token: address, amount: uint256, recipient: address) ``` :::note Reverts if any of the following is true: - called by anyone except the owner - ERC20 transfer reverted - `recipient` is zero address ::: --- # NodeOperatorsRegistry - [Source Code](https://github.com/lidofinance/core/blob/master/contracts/0.4.24/nos/NodeOperatorsRegistry.sol) - [Deployed Contract](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5) The `NodeOperatorsRegistry` contract acts as a registry of Node Operators selected by the Lido DAO. Since [Lido V2 upgrade](https://blog.lido.fi/introducing-lido-v2/) `NodeOperatorsRegistry` contract became a module of [`StakingRouter`](/contracts/staking-router) and got the second name **Curated staking module** as part of the general Lido staking infrastructure. As a staking module, `NodeOperatorsRegistry` implements [StakingModule interface](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/interfaces/IStakingModule.sol). `NodeOperatorsRegistry` keeps track of various Node Operators data, in particular limits of the allowed stake, reward addresses, penalty information, public keys of the Node Operators' validators. It defines order in which the Node Operators get the ether deposited and reward distribution between the node operators. The Lido DAO obliges a curated node operator to exit its validators timely if requested by the Lido protocol. The exit request is formed on-chain by the [`ValidatorsExitBusOracle`](/contracts/validators-exit-bus-oracle) contract. If a NO doesn't fulfil the request timely, it will be reported onchain and sanctions may be applied by the DAO. The Lido DAO can also: - set a target validator limit for the NO, as well as the priority exit mode. If the current active number of validators is above the target, the excess ones will be requested to exit in a prioritized manner when required to [finalize withdrawal requests](/contracts/withdrawal-queue-erc721#finalization). Allocation of deposits above the target value is prohibited. - deactivate misbehaving operators by `deactivateNodeOperator()`. A deactivated node operator does not receive rewards or new deposits. ## Glossary :::note In the context of these terms "signing key", "key", "validator key", "validator" might be used interchangeably. ::: **signing key**. BLS12-381 public key that will be used by the protocol for making Beacon deposits to [run a validator](/docs/guides/curated-module/validator-keys.md#generating-signing-keys) **vetted** (signing key). Approved by the Lido DAO for receiving ether for deposit. **submitted** (signing key). Added to the node operators registry. **depositable** (signing key). Suitable for new deposits. **deposited** (signing key). Ever received deposit. **unused** (signing key). Submitted but not deposited yet. **exited** (signing key). A validator that got into "Exited" state: either by [voluntary exit](https://lighthouse-book.sigmaprime.io/voluntary-exit.html) or as a result of slashing. [This doc](https://www.attestant.io/posts/understanding-the-validator-lifecycle/) might be useful regarding the validators lifecycle. **used (active)** (signing key). Deposited but not yet exited. **late** (validator). Not exited in proper time after an exit request from [`ValidatorsExitBusOracle`](/contracts/validators-exit-bus-oracle) by Lido protocol. **refunded** (stuck validator). Compensated by the NO for being stuck. For more information on handling of NO misbehavior see Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)). ## Node operator parameters For each NO the contract keeps a record of at least these values: - `active: bool` active/inactive status of the NO. An active NO gets rewards and new deposits according to its staking limit. New node operators are added in active state. - `name: string` human-readable name of the NO - `rewardAddress: address` where to send stETH rewards (part of the protocol fee) - `totalVettedValidators: uint64` Maximum number of validator keys approved for deposit by the DAO so far - `totalExitedValidators: uint64` incremental counter of all exited validators for the NO so far - `totalAddedValidators: uint64` incremental counter of all added to the NO validators so far - `totalDepositedValidators: uint64` incremental counter of all deposited validators for the NO so far - `targetValidatorsCount: uint256` target value for the number of validators for the NO. If the current active number of validators is above the value, the excess ones will be requested to exit. Allocation of deposits above the target value is prohibited. The exiting works only if `targetLimitMode` is non-zero. The `0` value will cause exit requests issued for all deposited validators of the NO. (see [VEBO](/guides/oracle-spec/validator-exit-bus) for details) - `targetLimitMode: uint256` NO's target limitation mode value (0 = disabled, 1 = smooth exit mode, 2 = boosted exit mode), determines whether the number of NO validators is target-limited, and if so, which exit mode will be applied on the [VEBO](/guides/oracle-spec/validator-exit-bus) side (see also `targetValidatorsCount`) - `stuckValidatorsCount: uint256` _deprecated_ number of stuck validators. Always returns 0. - `refundedValidatorsCount: uint256` _deprecated_ number of refunded validators. Always returns 0. - `depositableValidatorsCount: uint256` number of depositable validators The values can be viewed by means of `getNodeOperator()` and `getNodeOperatorSummary()`. Except for the functions listed below, the contract has methods accessible only by [`StakingRouter`](/contracts/staking-router) (holder of `STAKING_ROUTER_ROLE`). These functions are called internally in the course of [`AccountingOracle`](/contracts/accounting-oracle) report. ## View Methods ### getRewardsDistribution() Returns the rewards distribution proportional to the effective stake for each node operator ```solidity function getRewardsDistribution(uint256 _totalRewardShares) returns ( address[] recipients, uint256[] shares, bool[] penalized ); ``` | Name | Type | Description | | -------------------- | --------- | ------------------------------------------- | | `_totalRewardShares` | `uint256` | Total amount of reward shares to distribute | ### getActiveNodeOperatorsCount() Returns the number of active node operators. ```solidity function getActiveNodeOperatorsCount() returns (uint256); ``` ### getNodeOperator() Returns the node operator by id. ```solidity function getNodeOperator(uint256 _nodeOperatorId, bool _fullInfo) returns ( bool active, string name, address rewardAddress, uint64 totalVettedValidators, uint64 totalExitedValidators, uint64 totalAddedValidators, uint64 totalDepositedValidators ); ``` | Name | Type | Description | | ----------------- | --------- | -------------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_fullInfo` | `bool` | If true, name will be returned as well | ### getTotalSigningKeyCount() Returns the total number of signing keys of the node operator. ```solidity function getTotalSigningKeyCount(uint256 _nodeOperatorId) returns (uint256); ``` | Name | Type | Description | | ----------------- | --------- | ---------------- | | `_nodeOperatorId` | `uint256` | Node operator id | ### getUnusedSigningKeyCount() Returns the number of usable signing keys of the node operator. ```solidity function getUnusedSigningKeyCount(uint256 _nodeOperatorId) returns (uint256); ``` | Name | Type | Description | | ----------------- | --------- | ---------------- | | `_nodeOperatorId` | `uint256` | Node operator id | ### getSigningKey() Returns n-th signing key of the node operator. ```solidity function getSigningKey(uint256 _nodeOperatorId, uint256 _index) returns ( bytes key, bytes depositSignature, bool used ); ``` | Name | Type | Description | | ----------------- | --------- | --------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_index` | `uint256` | Index of the key, starting with 0 | Returns: | Name | Type | Description | | ------------------ | ------- | ----------------------------------------------------- | | `key` | `bytes` | Key | | `depositSignature` | `bytes` | Signature needed for a `depositContract.deposit` call | | `used` | `bool` | Flag indicating whether the key was used for staking | ### getSigningKeys() Returns subset of the signing keys of the node operator corresponding to the specified range `[_offset, _offset + _limit)`. If the requested range is out of bounds of the range `[0, )`, the call reverts with an `OUT_OF_RANGE` error. ```solidity function getSigningKeys(uint256 _nodeOperatorId, uint256 _offset, uint256 _limit) returns ( bytes memory pubkeys, bytes memory signatures, bool[] memory used ); ``` | Name | Type | Description | | ----------------- | --------- | ------------------------------------------------------------------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_offset` | `uint256` | Offset of the key in the array of all NO keys (`0` means the first key, `1` the second, etc | | `_limit` | `uint256` | Number of keys to return | Returns: | Name | Type | Description | | ------------ | -------- | --------------------------------------------------------------------------------------------------------- | | `pubkeys` | `bytes` | Keys concatenated into the bytes batch: `[ 48 bytes key \| 48 bytes key \| ... ]` | | `signatures` | `bytes` | Signatures needed for a `depositContract.deposit` call, concatenated as `[ 96 bytes \| 96 bytes \| ... ]` | | `used` | `bool[]` | Array of flags indicating whether the key was used for staking | ### getNodeOperatorsCount() Returns the total number of node operators. ```solidity function getNodeOperatorsCount() returns (uint256); ``` ### getNonce() Returns a counter that increments whenever the deposit data set changes. Namely, it increments every time when for a node operator: - staking limit changed; - target validators limit changed; - stuck validators count changed; - exited validators count changed; - validator signing keys added/removed; - penalty is cleared; - ready to deposit keys invalidated (due to withdrawal credentials change or due to manual invalidation by call of `invalidateReadyToDepositKeysRange`); - ether deposited. ```solidity function getNonce() view returns (uint256); ``` ### getType() Returns the type of the staking module. ```solidity function getType() view returns (bytes32); ``` ### getStakingModuleSummary() Returns some statistics of the staking module. ```solidity function getStakingModuleSummary() view returns ( uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount ); ``` | Name | Type | Description | | ---------------------------- | --------- | ------------------------------------------- | | `totalExitedValidators` | `uint256` | Total number of exited validators | | `totalDepositedValidators` | `uint256` | Total number of deposited validators | | `depositableValidatorsCount` | `uint256` | Number of validators which can be deposited | ### getNodeOperatorIsActive() Returns if the node operator with given id is active. ```solidity function getNodeOperatorIsActive(uint256 _nodeOperatorId) view returns (bool); ``` | Name | Type | Description | | ----------------- | --------- | ---------------- | | `_nodeOperatorId` | `uint256` | Node operator id | ### getNodeOperatorIds() Returns up to `_limit` node operator ids starting from the `_offset`. ```solidity function getNodeOperatorIds(uint256 _offset, uint256 _limit) view returns (uint256[] memory nodeOperatorIds); ``` | Name | Type | Description | | --------- | --------- | ---------------------------------------- | | `_offset` | `uint256` | Offset of the first element of the range | | `_limit` | `uint256` | Max number of NO ids to return | ### getNodeOperatorSummary() Returns some statistics of the node operator. ```solidity function getNodeOperatorSummary(uint256 _nodeOperatorId) view returns ( uint256 targetLimitMode, uint256 targetValidatorsCount, uint256 stuckValidatorsCount, uint256 refundedValidatorsCount, uint256 stuckPenaltyEndTimestamp, uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount ); ``` | Name | Type | Description | | ---------------------------- | --------- | ------------------------------------------------------------------------------------------------ | | `targetLimitMode` | `uint256` | Current target limit mode applied to the NO (0 = disabled, 1 = soft, 2 = boosted) | | `targetValidatorsCount` | `uint256` | Target validators count for full description see [parameters section](#node-operator-parameters) | | `stuckValidatorsCount` | `uint256` | _deprecated_ Number of stuck keys from oracle report | | `refundedValidatorsCount` | `uint256` | _deprecated_ Number of refunded keys | | `stuckPenaltyEndTimestamp` | `uint256` | _deprecated_ Extra penalty time after stuck keys refunded | | `totalExitedValidators` | `uint256` | Number of keys in the EXITED state of the NO for all time | | `totalDepositedValidators` | `uint256` | Number of keys of the NO which were in DEPOSITED state for all time | | `depositableValidatorsCount` | `uint256` | Number of validators which can be deposited | ### getStuckPenaltyDelay() _Deprecated_ Returns value of the stuck penalty delay (in seconds). This parameter defines how long a penalized NO stays in penalty state after the stuck keys were refunded. ```solidity function getStuckPenaltyDelay() view returns (uint256); ``` ### isOperatorPenalized() _Deprecated_ Returns flag whether the NO is penalized. ```solidity function isOperatorPenalized(uint256 _nodeOperatorId) view returns (bool) ``` ### isOperatorPenaltyCleared() _Deprecated_ Returns whether the NO penalty is cleared. ```solidity function isOperatorPenaltyCleared(uint256 _nodeOperatorId) view returns (bool) ``` ### getLocator() Returns the address of [`LidoLocator`](/contracts/lido-locator). ```solidity function getLocator() view returns (ILidoLocator) ``` ### getRewardDistributionState() Gets the current reward distribution state. Anyone can monitor this state and distribute rewards (by calling `distributeReward`) among operators when it is `ReadyForDistribution`. ```solidity enum RewardDistributionState { TransferredToModule, ReadyForDistribution, Distributed } ``` ```solidity function getRewardDistributionState() public view returns (RewardDistributionState); ``` **Returns:** | Name | Type | Description | | ----- | ----------------------- | --------------------------------- | | state | RewardDistributionState | Current reward distribution state | ## Methods ### addNodeOperator() Add node operator named `_name` with reward address `_rewardAddress` and staking limit = 0. Executed on behalf of holder of `MANAGE_NODE_OPERATOR_ROLE` role. ```solidity function addNodeOperator( string _name, address _rewardAddress ) returns (uint256 id); ``` | Name | Type | Description | | ---------------- | --------- | ------------------------------------------------------ | | `_name` | `string` | Human-readable name | | `_rewardAddress` | `address` | Address which receives stETH rewards for this operator | Returns: | Name | Type | Description | | ---- | --------- | ---------------------------------- | | `id` | `uint256` | A unique key of the added operator | ### activateNodeOperator() Activates deactivated node operator with given id. Executed on behalf of holder of `MANAGE_NODE_OPERATOR_ROLE` role. :::note Increases the validators keys nonce ::: ```solidity function activateNodeOperator(uint256 _nodeOperatorId); ``` | Name | Type | Description | | ----------------- | --------- | ---------------- | | `_nodeOperatorId` | `uint256` | Node operator id | ### deactivateNodeOperator() Deactivates active node operator with given id. Executed on behalf of holder of `MANAGE_NODE_OPERATOR_ROLE` role :::note Increases the validators keys nonce ::: ```solidity function deactivateNodeOperator(uint256 _nodeOperatorId); ``` | Name | Type | Description | | ----------------- | --------- | ---------------- | | `_nodeOperatorId` | `uint256` | Node operator id | ### setNodeOperatorName() Change human-readable name of the node operator with given id. Executed on behalf of holder of `MANAGE_NODE_OPERATOR_ROLE` role. ```solidity function setNodeOperatorName(uint256 _nodeOperatorId, string _name); ``` | Name | Type | Description | | ----------------- | --------- | ------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_name` | `string` | Human-readable name | ### setNodeOperatorRewardAddress() Change reward address of the node operator with given id. Executed on behalf of holder of `MANAGE_NODE_OPERATOR_ROLE` role. ```solidity function setNodeOperatorRewardAddress(uint256 _nodeOperatorId, address _rewardAddress); ``` | Name | Type | Description | | ----------------- | --------- | ------------------ | | `_nodeOperatorId` | `uint256` | Node operator id | | `_rewardAddress` | `address` | New reward address | ### setNodeOperatorStakingLimit() Set the maximum number of validators to stake for the node operator with given id. Executed on behalf of holder of `SET_NODE_OPERATOR_LIMIT_ROLE` role. :::note Current implementation preserves invariant: `depositedSigningKeysCount <= vettedSigningKeysCount <= totalSigningKeysCount`. If `_vettedSigningKeysCount` out of range `[depositedSigningKeysCount, totalSigningKeysCount]`, the new vettedSigningKeysCount value will be set to the nearest range border. ::: :::note Increases the validators keys nonce ::: ```solidity function setNodeOperatorStakingLimit(uint256 _nodeOperatorId, uint64 _vettedSigningKeysCount); ``` | Name | Type | Description | | ------------------------- | --------- | ----------------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id to set staking limit for | | `_vettedSigningKeysCount` | `uint64` | New staking limit of the node operator | ### addSigningKeys() Add `_keysCount` validator signing keys to the keys of the node operator `_nodeOperatorId`. Can be executed for the given NO if called from the NO's reward address or by the holder of `MANAGE_SIGNING_KEYS` role. :::note Along with each key `pubkey`, a signature must be provided for the `(pubkey, withdrawal_credentials, 32000000000)` message. For details, see the [keys section in the NO guide]. Given that information, the contract will be able to call `depositContract.deposit` on-chain. ::: :::note Increases the validators keys nonce ::: ```solidity function addSigningKeys( uint256 _nodeOperatorId, uint256 _keysCount, bytes _publicKeys, bytes _signatures ); ``` | Name | Type | Description | | ----------------- | --------- | --------------------------------------------------------------------------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_keysCount` | `uint256` | Number of signing keys provided | | `_publicKeys` | `bytes` | Several concatenated validator signing public keys | | `_signatures` | `bytes` | Several concatenated signatures for the DepositContract messages see the [keys section in NO guide] | [keys section in NO guide]: /guides/curated-module/validator-keys.md#generating-signing-keys ### removeSigningKeys() Removes an `_keysCount` of validator signing keys starting from `_fromIndex` of operator `_nodeOperatorId` usable keys. Can be executed for the given NO if called from the NO's reward address or by the holder of `MANAGE_SIGNING_KEYS` role. Keys are removed starting from the last index toward the highest one, so we won't go outside the array. :::note Increases the validators keys nonce ::: ```solidity function removeSigningKeys(uint256 _nodeOperatorId, uint256 _fromIndex, uint256 _keysCount); ``` | Name | Type | Description | | ----------------- | --------- | --------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_fromIndex` | `uint256` | Index of the key, starting with 0 | | `_keysCount` | `uint256` | Number of keys to remove | ### invalidateReadyToDepositKeysRange() Invalidates all unused validator keys for node operators in the given range. Executed on behalf of holder of `MANAGE_NODE_OPERATOR_ROLE` role. ```solidity function invalidateReadyToDepositKeysRange(uint256 _indexFrom, uint256 _indexTo); ``` | Name | Type | Description | | ------------ | --------- | ------------------------------------------------------------ | | `_indexFrom` | `uint256` | The first index (inclusive) of the NO to invalidate keys for | | `_indexTo` | `uint256` | The last index (inclusive) of the NO to invalidate keys for | ### distributeReward() Permissionless method for distributing all accumulated module rewards among node operators based on the latest accounting report. Rewards can be distributed after all necessary data required to distribute rewards among operators has been delivered, including exited and stuck keys. The reward distribution lifecycle (see also [getRewardDistributionState](#getrewarddistributionstate)): 1. TransferredToModule: Rewards are transferred to the module during an oracle main report. 2. ReadyForDistribution: All necessary data required to distribute rewards among operators has been delivered. 3. Distributed: Rewards have been successfully distributed. The function can only be called when the state is `ReadyForDistribution`. ```solidity function distributeReward() external; ``` ## reportValidatorExitDelay() Handles the tracking and penalization logic for a node operator who fails to exit their validator within the defined exit window. Marks a validator as late for the specified node operator using a proof timestamp and the elapsed eligibility-to-exit time. This information is then used by the module to apply exit-delay penalties to the node operator, if applicable. Called by the StakingRouter. ```solidity function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes _publicKey, uint256 _eligibleToExitInSec ) external; ``` | Name | Type | Description | | ---------------------- | --------- | ------------------------------------------------------------------------ | | `_nodeOperatorId` | `uint256` | Node operator id | | `_proofSlotTimestamp` | `uint256` | Beacon slot timestamp used as a proof reference for the validator status | | `_publicKey` | `bytes` | Validator BLS public key | | `_eligibleToExitInSec` | `uint256` | How many seconds the validator has been eligible to exit up to the proof | ## onValidatorExitTriggered() Handles a triggerable exit event for a validator belonging to a specific node operator. Called by the StakingRouter when a validator exit is initiated on the Execution Layer (via a withdrawal request). Records the trigger context and may affect the applicability of exit-delay penalties for the operator. ```solidity function onValidatorExitTriggered( uint256 _nodeOperatorId, bytes _publicKey, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external; ``` | Name | Type | Description | | --------------------------- | --------- | -------------------------------------------------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_publicKey` | `bytes` | Validator BLS public key | | `_withdrawalRequestPaidFee` | `uint256` | Fee paid to submit the withdrawal/exit request (units as defined by SR/WQ) | | `_exitType` | `uint256` | Exit trigger type code as defined by the StakingRouter | ## isValidatorExitDelayPenaltyApplicable() Determines whether a validator's exit delay should be considered when penalizing the node operator. Use this view to check if the module expects an update for the given validator and whether the elapsed eligibility-to-exit time indicates that an exit-delay penalty may apply. ```solidity function isValidatorExitDelayPenaltyApplicable( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool); ``` | Name | Type | Description | | ---------------------- | --------- | -------------------------------------------------------- | | `_nodeOperatorId` | `uint256` | Node operator id | | `_proofSlotTimestamp` | `uint256` | Beacon slot timestamp reference | | `_publicKey` | `bytes` | Validator BLS public key | | `_eligibleToExitInSec` | `uint256` | How many seconds the validator has been eligible to exit | Returns: | Name | Type | Description | | --------------------- | ------ | ------------------------------------------------------------------------- | | `isPenaltyApplicable` | `bool` | True if the exit-delay update should be accepted and may affect penalties | ## exitDeadlineThreshold() Returns the number of seconds after which a validator is considered late for a specified node operator. ```solidity function exitDeadlineThreshold(uint256 _nodeOperatorId) external view returns (uint256); ``` | Name | Type | Description | | ----------------- | --------- | ---------------- | | `_nodeOperatorId` | `uint256` | Node operator id | Returns: | Name | Type | Description | | ------------------- | --------- | --------------------------------------------------- | | `deadlineInSeconds` | `uint256` | Exit deadline threshold in seconds for specified NO | ## isValidatorExitingKeyReported() Returns whether a validator's key has already been reported as late for a specified node operator. ```solidity function isValidatorExitingKeyReported(uint256 _nodeOperatorId, bytes _publicKey) external view returns (bool); ``` | Name | Type | Description | | ----------------- | --------- | ------------------------ | | `_nodeOperatorId` | `uint256` | Node operator id | | `_publicKey` | `bytes` | Validator BLS public key | Returns: | Name | Type | Description | | --------------- | ------ | --------------------------------------------- | | `isKeyReported` | `bool` | True if the validator exit delay was reported | --- # OperatorGrid - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/OperatorGrid.sol) - [Deployed contract](https://etherscan.io/address/0xC69685E89Cefc327b43B7234AC646451B27c544d) Registry for node operators, groups, and tier parameters that define share limits, reserve ratios, and fee schedules for stVaults. ## What is OperatorGrid? OperatorGrid governs vault risk and fee parameters: - registers node operator groups - defines tier parameters (share limits, reserve ratio, fees) - applies tier changes to vaults with multi-party confirmation - enforces limits on liability shares and jailing status VaultHub consults OperatorGrid to validate connection parameters and minting constraints. ## Key concepts ### Groups A **group** represents a node operator in the OperatorGrid. Each group has: - A unique node operator address - A share limit (maximum liability shares across all their vaults) - One or more tiers defining risk parameters - Tracked liability shares across all group vaults Groups are registered via `registerGroup()` by addresses with `REGISTRY_ROLE`. ### Tiers A **tier** defines risk and fee parameters for vaults: - **Share limit**: Maximum liability shares for vaults in this tier - **Reserve ratio**: Minimum collateralization - **Forced rebalance threshold**: When to trigger force rebalance (must be at least 10bp below reserve ratio) - **Fees**: Infrastructure, liquidity, and reservation fees Each tier belongs to a specific node operator (except the default tier). ### Default tier All vaults start in the **default tier** (tier ID 0), which has a special operator address (`DEFAULT_TIER_OPERATOR`). The default tier parameters are set during initialization and apply to all newly connected vaults until they explicitly change to a node operator's tier. Vaults in the default tier only count against the default tier's share limit, not any group's limit. ### Tier changes Changing a vault's tier requires **multi-confirmation** from both parties. Either side can initiate: 1. Vault owner requests the tier change via Dashboard's `changeTier()` **or** the node operator requests it via OperatorGrid 2. The other party confirms by calling `changeTier()` with the same parameters 3. Once both confirm within the `confirmExpiry` window, the change is applied **Important constraints:** - Cannot change to the default tier (tier ID 0) - Cannot change to a tier owned by a different node operator - Node operators can pre-approve tier changes for disconnected vaults - Vault owners can only confirm when the vault is connected to VaultHub - Requested share limit must be between current liability shares and tier's share limit ### Tier sync When tier parameters are updated via `alterTiers()`, existing vaults are **not** automatically updated. Vault owners and node operators must call `syncTier()` with dual confirmation to apply the new tier parameters to their vault. ### Liability shares tracking OperatorGrid tracks `liabilityShares` (not `mintedShares`) at three levels: 1. **Tier level**: Sum of liability shares for all vaults in the tier 2. **Group level**: Sum of liability shares for all vaults in the group (excludes default tier) 3. **Vault level**: Tracked by VaultHub When shares are minted or burned, OperatorGrid updates the tier and group totals. ### Jailing Vaults can be "jailed" by addresses with `REGISTRY_ROLE`. A jailed vault: - Cannot mint new shares (normal minting is blocked) - Can still burn shares and rebalance - Administrative operations (like bad debt socialization) can bypass jail restrictions ## Constants | Constant | Value | Description | | ----------------------- | --------------- | ---------------------------------------------------- | | `DEFAULT_TIER_ID` | 0 | ID of the default tier | | `DEFAULT_TIER_OPERATOR` | `0xFFFF...FFFF` | Special address for default tier (type(uint160).max) | | `TOTAL_BASIS_POINTS` | 10000 | Basis points denominator | | `MAX_FEE_BP` | 65535 | Maximum fee in basis points (~655%) | | `MAX_RESERVE_RATIO_BP` | 9999 | Maximum reserve ratio (99.99%) | ## Structs ### Group Node operator group information: ```solidity struct Group { address operator; // Node operator address uint96 shareLimit; // Maximum liability shares across all group vaults uint96 liabilityShares; // Current liability shares in the group uint256[] tierIds; // Array of tier IDs belonging to this group } ``` ### Tier Tier parameters: ```solidity struct Tier { address operator; // Node operator (DEFAULT_TIER_OPERATOR for default tier) uint96 shareLimit; // Max liability shares for vaults in this tier uint96 liabilityShares; // Current liability shares in the tier uint16 reserveRatioBP; // Reserve ratio in basis points uint16 forcedRebalanceThresholdBP; // Force rebalance threshold in basis points uint16 infraFeeBP; // Infrastructure fee in basis points uint16 liquidityFeeBP; // Liquidity fee in basis points uint16 reservationFeeBP; // Reservation fee in basis points } ``` ### TierParams Parameters for registering or updating tiers: ```solidity struct TierParams { uint256 shareLimit; uint256 reserveRatioBP; uint256 forcedRebalanceThresholdBP; uint256 infraFeeBP; uint256 liquidityFeeBP; uint256 reservationFeeBP; } ``` ## View methods ### group(address \_nodeOperator) ```solidity function group(address _nodeOperator) external view returns (Group memory) ``` Returns group info for a node operator. ### nodeOperatorAddress(uint256 \_index) ```solidity function nodeOperatorAddress(uint256 _index) external view returns (address) ``` Returns node operator address by index. ### nodeOperatorCount() ```solidity function nodeOperatorCount() external view returns (uint256) ``` Returns number of registered node operators. ### tier(uint256 \_tierId) ```solidity function tier(uint256 _tierId) external view returns (Tier memory) ``` Returns tier parameters by ID. ### tiersCount() ```solidity function tiersCount() external view returns (uint256) ``` Returns total number of tiers (including default tier). ### vaultTierInfo(address \_vault) ```solidity function vaultTierInfo(address _vault) external view returns ( address nodeOperator, uint256 tierId, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ) ``` Returns effective tier configuration for a vault based on its current tier ID. ### effectiveShareLimit(address \_vault) ```solidity function effectiveShareLimit(address _vault) public view returns (uint256) ``` Returns the effective share limit for a vault, which is the minimum of: - Vault's configured share limit (from VaultHub connection) - Remaining capacity in the tier and group ### isVaultInJail(address \_vault) ```solidity function isVaultInJail(address _vault) external view returns (bool) ``` Returns whether a vault is jailed. ## Methods ### initialize(address \_admin, TierParams \_defaultTierParams) ```solidity function initialize(address _admin, TierParams calldata _defaultTierParams) external initializer ``` Initializes registry with admin and default tier parameters. ### setConfirmExpiry(uint256 \_newConfirmExpiry) ```solidity function setConfirmExpiry(uint256 _newConfirmExpiry) external ``` Sets confirmation expiry period for tier changes. Requires `REGISTRY_ROLE`. ### registerGroup(address \_nodeOperator, uint256 \_shareLimit) ```solidity function registerGroup(address _nodeOperator, uint256 _shareLimit) external ``` Registers a new node operator group. Requires `REGISTRY_ROLE`. ### updateGroupShareLimit(address \_nodeOperator, uint256 \_shareLimit) ```solidity function updateGroupShareLimit(address _nodeOperator, uint256 _shareLimit) external ``` Updates a group's share limit. Requires `REGISTRY_ROLE`. ### registerTiers(address \_nodeOperator, TierParams[] \_tiers) ```solidity function registerTiers( address _nodeOperator, TierParams[] calldata _tiers ) external ``` Registers one or more tiers for a node operator. Requires `REGISTRY_ROLE`. Group must exist. ### alterTiers(uint256[] \_tierIds, TierParams[] \_tierParams) ```solidity function alterTiers( uint256[] calldata _tierIds, TierParams[] calldata _tierParams ) external ``` Updates existing tier parameters. Requires `REGISTRY_ROLE`. Does not automatically update existing vaults - they must call `syncTier()`. ### changeTier(address \_vault, uint256 \_requestedTierId, uint256 \_requestedShareLimit) ```solidity function changeTier( address _vault, uint256 _requestedTierId, uint256 _requestedShareLimit ) external returns (bool) ``` Requests tier change for a vault. Returns `true` if executed, `false` if awaiting confirmation. **Requirements:** - Cannot change to default tier (ID 0) - Requested tier must belong to the vault's node operator - Requested share limit must be โ‰ค tier's share limit - Requested share limit must be โ‰ฅ vault's current liability shares - Both vault owner and node operator must confirm - Tier and group limits must not be exceeded ### syncTier(address \_vault) ```solidity function syncTier(address _vault) external returns (bool) ``` Syncs vault's connection parameters with current tier parameters. Returns `true` if executed, `false` if awaiting confirmation. Requires dual confirmation. Vault must be connected and not already in sync. ### updateVaultShareLimit(address \_vault, uint256 \_requestedShareLimit) ```solidity function updateVaultShareLimit(address _vault, uint256 _requestedShareLimit) external returns (bool) ``` Updates a vault's share limit within its current tier. Returns `true` if executed, `false` if awaiting confirmation. Requires dual confirmation. **Requirements:** - Requested limit must be โ‰ค tier's share limit - Requested limit must be โ‰ฅ vault's current liability shares - Must not be same as current limit ### resetVaultTier(address \_vault) ```solidity function resetVaultTier(address _vault) external ``` Resets vault to the default tier. Only callable by VaultHub (on disconnect). ### updateVaultFees(...) ```solidity function updateVaultFees( address _vault, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) external ``` Updates per-vault fee parameters independently of tier. Requires `REGISTRY_ROLE`. Vault must be connected. ### onMintedShares(address \_vault, uint256 \_amount, bool \_overrideLimits) ```solidity function onMintedShares( address _vault, uint256 _amount, bool _overrideLimits ) external ``` Notifies OperatorGrid about minted shares. Only callable by VaultHub. **Checks (unless `_overrideLimits` is true):** - Vault must not be jailed - Tier limit must not be exceeded - Group limit must not be exceeded (for non-default tiers) ### onBurnedShares(address \_vault, uint256 \_amount) ```solidity function onBurnedShares(address _vault, uint256 _amount) external ``` Notifies OperatorGrid about burned shares. Only callable by VaultHub. Decrements tier and group liability shares. ### setVaultJailStatus(address \_vault, bool \_isInJail) ```solidity function setVaultJailStatus(address _vault, bool _isInJail) external ``` Jails or unjails a vault. Requires `REGISTRY_ROLE`. ## Permissions | Role | Description | | -------------------- | --------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | Admin role for granting/revoking other roles | | `REGISTRY_ROLE` | Can register groups/tiers, update limits, jail vaults, set fees | ## Related - [VaultHub](/contracts/vault-hub) - [StakingVault](/contracts/staking-vault) - [Dashboard](/contracts/dashboard) - [stVaults Parameters and Metrics](/run-on-lido/stvaults/features-and-mechanics/parameters-and-metrics) --- # OracleDaemonConfig - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/OracleDaemonConfig.sol) - [Deployed contract](https://etherscan.io/address/0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09) OracleDaemonConfig acts as a parameters registry for the Lido oracle daemon. The full list of params is tracked in the OracleDaemonConfig contract on mainnet. :::note In contrast to [`OracleReportSanityChecker`](/contracts/oracle-report-sanity-checker), the stored values aren't enforced by the protocol on-chain code. ::: ## View methods ### get(string calldata \_key) Retrieves the value corresponding to the provided key. ```solidity function get(string calldata _key) external view returns (bytes memory) ``` :::note Reverts if value is missing. ::: ### getList(string[] calldata \_keys) Retrieves a list of values corresponding to the provided keys. ```solidity function getList(string[] calldata _keys) external view returns (bytes[] memory) ``` :::note Reverts if any value for a specific key is missing. ::: ## Methods ### set(string calldata \_key, bytes calldata \_value) Sets the value for the provided key. Can only be called by users with `CONFIG_MANAGER_ROLE`. ```solidity function set(string calldata _key, bytes calldata _value) external ``` :::note Reverts if any of the following is true: - value with provided key already exists - value is empty - called by someone who doesn't have `CONFIG_MANAGER_ROLE` role ::: ### update(string calldata \_key, bytes calldata \_value) Updates the value for the provided key. Can only be called by users with `CONFIG_MANAGER_ROLE`. ```solidity function update(string calldata _key, bytes calldata _value) external ``` :::note Reverts if any of the following is true: - value with provided key doesn't exist - value is the same with the one already set - value is empty - called by someone who doesn't have `CONFIG_MANAGER_ROLE` role ::: ### unset(string calldata \_key) Removes the value of the provided key. Can only be called by users with `CONFIG_MANAGER_ROLE`. ```solidity function unset(string calldata _key) external ``` :::note Reverts if any of the following is true: - value with provided key doesn't exist - called by someone who doesn't have `CONFIG_MANAGER_ROLE` role ::: ## Events ### ConfigValueSet Emitted when a new key-value pair is set. ```solidity event ConfigValueSet(string indexed key, bytes value) ``` ### ConfigValueUpdated Emitted when a key-value pair is updated. ```solidity event ConfigValueUpdated(string indexed key, bytes value) ``` ### ConfigValueUnset Emitted when a key-value pair is unset. ```solidity event ConfigValueUnset(string indexed key) ``` --- # OracleReportSanityChecker - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol) - [Deployed contract](https://etherscan.io/address/0x147f8d3cf3004FAf9Bf94E88B54b6C06De507be9) Some vital data for the Lido protocol is collected off-chain and delivered on-chain via Oracle contracts: [`AccountingOracle`](/contracts/accounting-oracle), [`ValidatorsExitBusOracle`](/contracts/validators-exit-bus-oracle). Due to the high impact of data provided by the Oracles on the state of the protocol, each Oracle's report passes a set of onchain [sanity checks](https://en.wikipedia.org/wiki/Sanity_check). For the simplicity of the contracts responsible for handling Oracle's reports, all sanity checks were collected in the standalone `OracleReportSanityChecker` contract. Besides the validation methods, the `OracleReportSanityChecker` contract contains a [set of tunable limits and restrictions](#limits-list) used during the report validation process. To configure the limits values contract provides the lever methods described in the [standalone section](#lever-methods). Access to lever methods is restricted using the functionality of the [AccessControlEnumerable](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/utils/access/AccessControlEnumerable.sol) contract and a bunch of [granular roles](#permissions). ## Limits List `OracleReportSanityChecker` uses the `LimitsList` type which contains all the limits used by the contract. ```solidity struct LimitsList { uint256 exitedEthAmountPerDayLimit; uint256 appearedEthAmountPerDayLimit; uint256 annualBalanceIncreaseBPLimit; uint256 simulatedShareRateDeviationBPLimit; uint256 maxBalanceExitRequestedPerReportInEth; uint256 maxEffectiveBalanceWeightWCType01; uint256 maxEffectiveBalanceWeightWCType02; uint256 maxItemsPerExtraDataTransaction; uint256 maxNodeOperatorsPerExtraDataItem; uint256 requestTimestampMargin; uint256 maxPositiveTokenRebase; uint256 maxCLBalanceDecreaseBP; uint256 clBalanceOraclesErrorUpperBPLimit; uint256 consolidationEthAmountPerDayLimit; uint256 exitedValidatorEthAmountLimit; uint256 externalPendingBalanceCapEth; } ``` - **`exitedEthAmountPerDayLimit` โˆˆ [0, type(uint32).max]** โ€” the max possible _**exited**_ ETH amount that might be reported per single day, denominated in whole ETH. Used together with `consolidationEthAmountPerDayLimit` in the [`checkExitedValidatorsCount()`](#checkexitedvalidatorscount) check. - **`appearedEthAmountPerDayLimit` โˆˆ [0, type(uint32).max]** โ€” the max possible _**appeared**_ (activated) ETH amount that might be reported per single day, denominated in whole ETH. Covers the Consensus Layer activation churn of up to 256 ETH per epoch (225 epochs ร— 256 ETH = 57,600 ETH per day). - **`annualBalanceIncreaseBPLimit` โˆˆ [0, 10000]** โ€” the max annual increase of the total validators' balances on the Consensus Layer since the previous oracle report. Represented in the [Basis Points](https://en.wikipedia.org/wiki/Basis_point) (100% == 10000). - **`simulatedShareRateDeviationBPLimit` โˆˆ [0, 10000]** โ€” the max deviation of the provided `simulatedShareRate` and the actual one within the currently processing oracle report. Represented in the [Basis Points](https://en.wikipedia.org/wiki/Basis_point) (100% == 10000). - **`maxBalanceExitRequestedPerReportInEth` โˆˆ [0, 65535]** โ€” the max total balance requested to exit per single report to [ValidatorsExitBusOracle](./validators-exit-bus-oracle.md), denominated in whole ETH. The sum of the max effective balances of all validators requested to exit in one report must be equal or lower than this value. - **`maxEffectiveBalanceWeightWCType01` โˆˆ [1, 65535]** โ€” the max effective balance equivalent weight in ETH for a validator with the `0x01` type withdrawal credentials (32 ETH). Used to calculate the total balance requested to exit. - **`maxEffectiveBalanceWeightWCType02` โˆˆ [1, 65535]** โ€” the max effective balance equivalent weight in ETH for a validator with the `0x02` type withdrawal credentials (2048 ETH, [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) MaxEB). Used to calculate the total balance requested to exit. - **`maxItemsPerExtraDataTransaction` โˆˆ [0, 65535]** โ€” the max number of data list items reported to accounting oracle in extra data per single transaction. - **`maxNodeOperatorsPerExtraDataItem` โˆˆ [0, 65535]** โ€” the max number of node operators reported per extra data list item - **`requestTimestampMargin` โˆˆ [0, type(uint32).max]** โ€” the min time required to be passed from the creation of the request to be finalized till the time of the oracle report - **`maxPositiveTokenRebase` โˆˆ [1, type(uint64).max]** โ€” the max positive token rebase allowed per single oracle report token rebase happens on total supply adjustment, huge positive rebase can incur oracle report sandwiching. Uses 1e9 precision, e.g.: `1e6` โ€” 0.1%; `1e9` โ€” 100%; `type(uint64).max` โ€” unlimited rebase. - **`maxCLBalanceDecreaseBP` โˆˆ [0, 10000]** โ€” the max allowed Consensus Layer balance decrease over the 36-day sliding window (`CL_BALANCE_WINDOW`) as a fraction of the expected balance restored from historical report snapshots. Represented in the [Basis Points](https://en.wikipedia.org/wiki/Basis_point) (100% == 10000). - **`clBalanceOraclesErrorUpperBPLimit` โˆˆ [0, 10000]** - the maximum percent on how Second Opinion Oracle reported value could be greater than reported by the AccountingOracle. There is an assumption that second opinion oracle CL balance can be greater as calculated for the withdrawal credentials. Represented in the [Basis Points](https://en.wikipedia.org/wiki/Basis_point) (100% == 10000). - **`consolidationEthAmountPerDayLimit` โˆˆ [0, type(uint32).max]** โ€” the max possible _**consolidated**_ ETH amount that might be reported per single day, denominated in whole ETH. Extends the per-module validators balance increase budget and the exited ETH amount per day limit to account for validator consolidations. - **`exitedValidatorEthAmountLimit` โˆˆ [1, 65535]** โ€” the effective ETH amount attributed to a single exited validator in the exited ETH amount per day check, denominated in whole ETH. - **`externalPendingBalanceCapEth` โˆˆ [0, 65535]** โ€” the extra protocol-level pending balance cap to tolerate bounded side deposits or same-validator top-ups that were not funded by Lido, denominated in whole ETH. Internally, the limits are persisted in two packed structs, each occupying a single storage slot: ```solidity struct AccountingCoreLimitsPacked { uint32 exitedEthAmountPerDayLimit; uint32 appearedEthAmountPerDayLimit; uint32 consolidationEthAmountPerDayLimit; uint16 annualBalanceIncreaseBPLimit; uint16 simulatedShareRateDeviationBPLimit; uint64 maxPositiveTokenRebase; uint16 maxCLBalanceDecreaseBP; uint16 clBalanceOraclesErrorUpperBPLimit; uint16 exitedValidatorEthAmountLimit; uint16 externalPendingBalanceCapEth; } struct OperationalLimitsPacked { uint16 maxBalanceExitRequestedPerReportInEth; uint16 maxEffectiveBalanceWeightWCType01; uint16 maxEffectiveBalanceWeightWCType02; uint16 maxItemsPerExtraDataTransaction; uint16 maxNodeOperatorsPerExtraDataItem; uint32 requestTimestampMargin; } ``` The [`getOracleReportLimits()`](#getoraclereportlimits) view method returns the limits unpacked into the `LimitsList` type. There is also parameter for Second Opinion Oracle which is not a part of the `LimitsList` structure. However it's modification requires the same type of roles as for the Limits. It could be changed with a general `setOracleReportLimits()` function or specific `setSecondOpinionOracleAndCLBalanceUpperMargin()`. For the details about meaning of the parameters `clBalanceOraclesErrorUpperBPLimit` and `Second Opinion Oracle` please refer to [LIP-23](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-23.md). ## Sanity Checks ### checkAccountingOracleReport() Applies sanity checks to the accounting parameters of Lido's Oracle report. Called by the [`Accounting`](/contracts/accounting) contract during oracle report processing; reverts with `CalledNotFromAccounting()` error when called by any other address. The method has side effects: on each successful accounting report it stores a report snapshot (timestamp, CL balance, deposits, CL withdrawals) used by the CL balance decrease check over a sliding window, and records the withdrawal vault balance remaining after the report's transfer. The CL balance decrease check operates over a sliding window of up to 36 days (`CL_BALANCE_WINDOW`). It restores the expected CL balance from the earliest stored snapshot within the window (baseline CL balance plus deposits minus actual CL withdrawals accumulated after the baseline) and compares it with the post-report CL balance (CL validators balance plus CL pending balance). The decrease is limited by `LimitsList.maxCLBalanceDecreaseBP` of the restored balance. When the decrease exceeds the limit and a Second Opinion Oracle is configured, the report is accepted only if the Second Opinion Oracle confirms the CL validators balance (within `LimitsList.clBalanceOraclesErrorUpperBPLimit`) and the exact withdrawal vault balance. :::note Below is the list of restrictions checked by the method execution: - Revert with `IncorrectWithdrawalsVaultBalance(uint256 actualWithdrawalVaultBalance)` error when the reported withdrawals vault balance **is greater than** the actual balance of the withdrawal vault. - Revert with `IncorrectELRewardsVaultBalance(uint256 actualELRewardsVaultBalance)` error when reported EL rewards vault balance **is greater than** the actual balance of EL rewards vault. - Revert with `IncorrectSharesRequestedToBurn(uint256 actualSharesToBurn)` error when the amount of stETH shares requested to burn **exceeds** the number of shares marked to be burned in the Burner contract. - Revert with `IncorrectCLWithdrawalsVaultBalance(uint256 withdrawalVaultBalance, uint256 lastWithdrawalVaultBalanceAfterTransfer)` error when the reported withdrawal vault balance **is lower than** the vault balance stored after the previous report's transfer. - Revert with `IncorrectWithdrawalsVaultTransfer(uint256 withdrawalVaultBalance, uint256 withdrawalsVaultTransfer)` error when the reported withdrawal vault transfer **exceeds** the reported withdrawal vault balance. - Revert with `IncorrectTotalPendingBalance(uint256 maxAllowed, uint256 actual)` error when the reported post-report CL pending balance **exceeds** the Lido-funded pending balance (pre-report CL pending balance plus deposits) plus `LimitsList.externalPendingBalanceCapEth`. - Revert with `IncorrectTotalActivatedBalance(uint256 maxAllowed, uint256 actual)` error when the balance activated from the pending deposits queue **exceeds** the allowance derived from `LimitsList.appearedEthAmountPerDayLimit` prorated by the elapsed time, plus a single max validator effective balance (2048 ETH) as a report-window boundary allowance. - Revert with `IncorrectTotalCLBalanceIncrease(uint256 maxAllowed, uint256 actual)` error when the increase of the CL validators balance (net of the CL withdrawals observed since the previous report) **exceeds** the activated balance plus the APR safety cap derived from `LimitsList.annualBalanceIncreaseBPLimit`. - Revert with `IncorrectCLBalanceDecrease(uint256 negativeCLRebaseSum, uint256 maxNegativeCLRebaseSum)` error when the Consensus Layer balance decrease over the sliding window **exceeds** the allowed maximum and no Second Opinion Oracle is configured. - Revert with `NegativeRebaseFailedCLBalanceMismatch(uint256 reportedValue, uint256 provedValue, uint256 limitBP)` error when the CL validators balance reported by oracles and Second Opinion Oracle is too different. - Revert with `NegativeRebaseFailedWithdrawalVaultBalanceMismatch(uint256 reportedValue, uint256 provedValue)` error when Withdrawal vault balance reported by oracles and second opinion oracle is different. - Revert with `NegativeRebaseFailedSecondOpinionReportIsNotReady()` error when second opinion oracle report is not available. - Revert with `IncorrectCLBalanceIncrease(uint256 annualBalanceDiff)` error when Consensus Layer annual balance increase expressed in basis points **exceeds** allowed `LimitsList.annualBalanceIncreaseBPLimit`. ::: ```solidity function checkAccountingOracleReport( uint256 _timeElapsed, uint256 _preCLValidatorsBalance, uint256 _preCLPendingBalance, uint256 _postCLValidatorsBalance, uint256 _postCLPendingBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, uint256 _sharesRequestedToBurn, uint256 _deposits, uint256 _withdrawalsVaultTransfer ) ``` #### Arguments - **`_timeElapsed`** โ€” time elapsed since the previous oracle report, measured in **seconds** - **`_preCLValidatorsBalance`** โ€” sum of all Lido validators' balances on the Consensus Layer (excluding pending deposits) before the current oracle report - **`_preCLPendingBalance`** โ€” CL pending balance (Lido-attributed pending deposits) before the current oracle report - **`_postCLValidatorsBalance`** โ€” sum of all Lido validators' balances on the Consensus Layer (excluding pending deposits) after the current oracle report - **`_postCLPendingBalance`** โ€” CL pending balance (Lido-attributed pending deposits) after the current oracle report - **`_withdrawalVaultBalance`** โ€” withdrawal vault balance on Execution Layer for the report reference slot - **`_elRewardsVaultBalance`** โ€” el rewards vault balance on Execution Layer for the report reference slot - **`_sharesRequestedToBurn`** โ€” shares requested to burn for the report reference slot - **`_deposits`** โ€” deposits to the Beacon Chain since the previous oracle report, in wei - **`_withdrawalsVaultTransfer`** โ€” ETH amount transferred from the withdrawal vault during the current report ### checkModuleAndCLBalancesChangeRates() Checks the per-module validator balances consistency and the global CL growth budget derived from the protocol pending balance, all in wei. Called by [`AccountingOracle`](/contracts/accounting-oracle) during main report submission, before the per-module validator balances are stored in the [`StakingRouter`](/contracts/staking-router). The per-module validators balance increase check aggregates the positive deltas between the reported per-module validator balances and the balances stored in the `StakingRouter`, and compares the sum with the activated balance budget plus the consolidation allowance derived from `LimitsList.consolidationEthAmountPerDayLimit`. The check is skipped until the first successful accounting report is finalized (see [`isPostMigrationFirstReportDone()`](#ispostmigrationfirstreportdone)) and, per module, while the module has no previous accounting baseline in the `StakingRouter`. :::note Below is the list of restrictions checked by the method execution: - Revert with `InvalidClBalancesData()` error when the lengths of the module ids and balances arrays differ. - Revert with `InconsistentValidatorsBalanceByModule(uint256 expected, uint256 actual)` error when the sum of the per-module validator balances **is not equal to** the reported total CL validators balance. - Revert with `IncorrectTotalPendingBalance(uint256 maxAllowed, uint256 actual)` error when the reported post-report CL pending balance **exceeds** the Lido-funded pending balance plus `LimitsList.externalPendingBalanceCapEth`. - Revert with `IncorrectTotalActivatedBalance(uint256 maxAllowed, uint256 actual)` error when the balance activated from the pending deposits queue **exceeds** the allowance derived from `LimitsList.appearedEthAmountPerDayLimit`. - Revert with `IncorrectTotalCLBalanceIncrease(uint256 maxAllowed, uint256 actual)` error when the increase of the CL validators balance **exceeds** the activated balance plus the APR safety cap. - Revert with `IncorrectTotalModuleValidatorsBalanceIncrease(uint256 maxAllowed, uint256 actual)` error when the sum of the positive per-module validator balance increases **exceeds** the activated balance budget plus the consolidation allowance. ::: ```solidity function checkModuleAndCLBalancesChangeRates( uint256[] calldata _stakingModuleIdsWithUpdatedBalance, uint256[] calldata _validatorBalancesWeiByStakingModule, uint256 _preCLValidatorsBalanceWei, uint256 _preCLPendingBalanceWei, uint256 _postCLValidatorsBalanceWei, uint256 _postCLPendingBalanceWei, uint256 _depositsWei, uint256 _timeElapsed ) ``` #### Arguments - **`_stakingModuleIdsWithUpdatedBalance`** โ€” ids of the staking modules with updated validator balances - **`_validatorBalancesWeiByStakingModule`** โ€” reported validator balances per staking module, in wei - **`_preCLValidatorsBalanceWei`** โ€” CL validators balance (excluding pending deposits) before the current oracle report, in wei - **`_preCLPendingBalanceWei`** โ€” CL pending balance before the current oracle report, in wei - **`_postCLValidatorsBalanceWei`** โ€” CL validators balance (excluding pending deposits) after the current oracle report, in wei - **`_postCLPendingBalanceWei`** โ€” CL pending balance after the current oracle report, in wei - **`_depositsWei`** โ€” deposits to the Beacon Chain since the previous oracle report, in wei - **`_timeElapsed`** โ€” time elapsed since the previous oracle report, measured in **seconds** ### checkExitBusOracleReport() Validates that the total balance requested to exit per oracle report does not exceed the limit set by `LimitsList.maxBalanceExitRequestedPerReportInEth`. The [`ValidatorsExitBusOracle`](/contracts/validators-exit-bus-oracle) calculates the total by attributing to each validator requested to exit the max effective balance weight of its withdrawal credentials type: `LimitsList.maxEffectiveBalanceWeightWCType01` for the `0x01` type and `LimitsList.maxEffectiveBalanceWeightWCType02` for the `0x02` type. :::note Reverts with `IncorrectSumOfExitBalancePerReport(uint256 maxBalanceSum)` error when check is failed. ::: ```solidity function checkExitBusOracleReport(uint256 _maxBalanceExitRequestedPerReportInEth) ``` #### Arguments - **`_maxBalanceExitRequestedPerReportInEth`** โ€” total balance in ETH of all validators requested to exit in the oracle report ### checkExitedValidatorsCount() Validates the newly exited validators count against the exited ETH amount per day limit. Called by [`AccountingOracle`](/contracts/accounting-oracle) after the exited validators counts per staking module are submitted to the [`StakingRouter`](/contracts/staking-router). :::note Reverts with `ExitedEthAmountPerDayLimitExceeded(uint256 limitPerDay, uint256 exitedPerDay)` error when check is failed. ::: ```solidity function checkExitedValidatorsCount( uint256 _newlyExitedValidatorsCount, uint256 _timeElapsed ) ``` #### Arguments - **`_newlyExitedValidatorsCount`** โ€” number of newly exited validators since the previous report - **`_timeElapsed`** โ€” time elapsed since the previous oracle report, measured in **seconds** ### checkNodeOperatorsPerExtraDataItemCount() Validates that number of node operators reported per extra data item does not exceed the limit set by `LimitsList.maxNodeOperatorsPerExtraDataItem`. :::note Reverts with `TooManyNodeOpsPerExtraDataItem(uint256 itemIndex, uint256 nodeOpsCount)` error when check is failed. ::: ```solidity function checkNodeOperatorsPerExtraDataItemCount( uint256 _itemIndex, uint256 _nodeOperatorsCount ) ``` #### Arguments - **`_itemIndex`** โ€” index of item in extra data - **`_nodeOperatorsCount`** โ€” number of node operators reported per the extra data item ### checkExtraDataItemsCountPerTransaction() Validates that number of extra data items per transaction in the report does not exceed the limit set by `LimitsList.maxItemsPerExtraDataTransaction`. :::note Reverts with `TooManyItemsPerExtraDataTransaction(uint256 maxItemsCount, uint256 receivedItemsCount)` error when check is failed. ::: ```solidity function checkExtraDataItemsCountPerTransaction(uint256 _extraDataListItemsCount) ``` #### Arguments - **`_extraDataListItemsCount`** โ€” number of items per single transaction in the accounting oracle report ### checkWithdrawalQueueOracleReport() Validates that withdrawal request with the passed `_lastFinalizableRequestId` was created more than `LimitsList.requestTimestampMargin` seconds ago. :::note Reverts with `IncorrectRequestFinalization(uint256 requestCreationTimestamp)` error when check is failed. ::: ```solidity function checkWithdrawalQueueOracleReport( uint256 _lastFinalizableRequestId, uint256 _reportTimestamp ) ``` #### Arguments - **`_lastFinalizableRequestId`** โ€” last finalizable withdrawal request id - **`_reportTimestamp`** โ€” timestamp when the originated oracle report was submitted ### checkSimulatedShareRate() Applies sanity checks to the simulated share rate for withdrawal requests finalization. :::note Reverts with `IncorrectSimulatedShareRate(uint256 simulatedShareRate, uint256 actualShareRate)` error when simulated share rate deviation exceeds the limit set by `LimitsList.simulatedShareRateDeviationBPLimit` ::: ```solidity function checkSimulatedShareRate( uint256 _postInternalEther, uint256 _postInternalShares, uint256 _etherToFinalizeWQ, uint256 _sharesToBurnForWithdrawals, uint256 _simulatedShareRate ) ``` #### Arguments - **`_postInternalEther`** โ€” total pooled ether after report applied - **`_postInternalShares`** โ€” total shares after report applied - **`_etherToFinalizeWQ`** โ€” ether locked on withdrawal queue for the current oracle report - **`_sharesToBurnForWithdrawals`** โ€” shares burnt due to withdrawals finalization - **`_simulatedShareRate`** โ€” share rate provided with the oracle report (simulated via off-chain `eth_call`) ## Migration ### migrateBaselineSnapshot() One-time permissionless method that seeds the initial snapshots into the [`reportData`](#reportdata) array so that the sliding-window CL balance decrease check has a valid starting point. The method is permissionless by design: after the first successful call, further calls revert. :::note - Reverts with `MigrationAlreadyDone()` error when the `reportData` array is not empty. - Reverts with `UnexpectedLidoVersion(uint256 actual, uint256 expected)` error when the `Lido` contract version is not `4`. - Emits `BaselineSnapshotMigrated(uint256 clBalance, uint256 clWithdrawals)`. ::: ```solidity function migrateBaselineSnapshot() ``` ## View Methods ### getLidoLocator() Returns the address of the protocol-wide [LidoLocator](/contracts/lido-locator) instance. ```solidity function getLidoLocator() returns (address) ``` ### getOracleReportLimits() Returns the limits list used for the sanity checks as the [`LimitsList`](#limits-list) type. ```solidity function getOracleReportLimits() returns (LimitsList memory) ``` ### getReportDataCount() Returns the number of report snapshots stored in the [`reportData`](#reportdata) array. ```solidity function getReportDataCount() returns (uint256) ``` ### reportData() Public array of historical report snapshots used by the CL balance decrease check over the sliding window. Each snapshot stores the report-window timestamp, the total CL balance (CL validators balance plus CL pending balance), the deposits for the period since the previous report, and the actual ETH moved from the Consensus Layer to the withdrawal vault during the period. ```solidity function reportData(uint256 _index) returns ( uint64 timestamp, uint128 clBalance, uint128 deposits, uint128 clWithdrawals ) ``` ### secondOpinionOracle() Returns the address of the Second Opinion Oracle (zero address means the Second Opinion Oracle is disabled). ```solidity function secondOpinionOracle() returns (ISecondOpinionOracle) ``` ### lastVaultBalanceAfterTransfer() Returns the withdrawal vault balance after the last report's transfer was applied. Used to compute the actual CL withdrawals as `current vault balance - lastVaultBalanceAfterTransfer`. ```solidity function lastVaultBalanceAfterTransfer() returns (uint256) ``` ### lastReportTimestamp() Returns the timestamp of the latest stored report snapshot used by the CL balance decrease window. It is advanced by the elapsed time on each accounting report. ```solidity function lastReportTimestamp() returns (uint256) ``` ### isPostMigrationFirstReportDone() Returns `false` until the first successful accounting report. The per-module validators balance increase check of [`checkModuleAndCLBalancesChangeRates()`](#checkmoduleandclbalanceschangerates) is skipped while the flag is `false`. ```solidity function isPostMigrationFirstReportDone() returns (bool) ``` ### getMaxCLBalanceDecreaseBP() Returns the `LimitsList.maxCLBalanceDecreaseBP` value. ```solidity function getMaxCLBalanceDecreaseBP() returns (uint256) ``` ### getMaxEffectiveBalanceWeightWCType01() Returns the `LimitsList.maxEffectiveBalanceWeightWCType01` value. ```solidity function getMaxEffectiveBalanceWeightWCType01() returns (uint256) ``` ### getMaxEffectiveBalanceWeightWCType02() Returns the `LimitsList.maxEffectiveBalanceWeightWCType02` value. ```solidity function getMaxEffectiveBalanceWeightWCType02() returns (uint256) ``` ### getMaxPositiveTokenRebase() Returns max positive token rebase value with 1e9 precision (e.g.: `1e6` โ€” 0.1%; `1e9` โ€” 100%): :::note Special values: - `0` (zero value) means uninitialized - `type(uint64).max` means unlimited, e.g. not enforced ::: Get max positive rebase allowed per single oracle report. Token rebase happens on total supply and/or total shares adjustment, while huge positive rebase can incur oracle report sandwiching stealing part of the stETH holders' rewards. The relative positive rebase value derived as follows: stETH balance for the `account` defined as: ```solidity balanceOf(account) = shares[account] * totalPooledEther / totalShares = shares[account] * shareRate ``` Suppose shareRate changes when oracle reports (see `Accounting.handleOracleReport`) which means that token rebase happens: ```solidity preShareRate = preTotalPooledEther() / preTotalShares() postShareRate = postTotalPooledEther() / postTotalShares() R = (postShareRate - preShareRate) / preShareRate ``` here `R > 0` corresponds to the relative positive rebase value (i.e., instant APR). ```solidity function getMaxPositiveTokenRebase() returns (uint256) ``` ### smoothenTokenRebase() Evaluates the following amounts during Lido's oracle report processing: - the allowed ETH amount that might be taken from the withdrawal vault and EL rewards vault - the allowed amount of stETH shares to be burnt ```solidity function smoothenTokenRebase( uint256 _preInternalEther, uint256 _preInternalShares, uint256 _preCLBalance, uint256 _postCLBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, uint256 _sharesRequestedToBurn, uint256 _etherToLockForWithdrawals, uint256 _newSharesToBurnForWithdrawals ) returns ( uint256 withdrawals, uint256 elRewards, uint256 sharesFromWQToBurn, uint256 sharesToBurn ) ``` #### Arguments - **`_preInternalEther`** โ€” amount of internal ETH controlled by the protocol - **`_preInternalShares`** โ€” number of internal shares - **`_preCLBalance`** โ€” sum of all Lido validators' active and pending balances on the Consensus Layer plus the deposits since the previous report, before the current oracle report - **`_postCLBalance`** โ€” sum of all Lido validators' active and pending balances on the Consensus Layer after the current oracle report - **`_withdrawalVaultBalance`** โ€” withdrawal vault balance on Execution Layer for the report calculation moment - **`_elRewardsVaultBalance`** โ€” elRewards vault balance on Execution Layer for the report calculation moment - **`_sharesRequestedToBurn`** โ€” shares requested to burn through Burner for the report calculation moment - **`_etherToLockForWithdrawals`** โ€” ether to lock on withdrawals queue contract - **`_newSharesToBurnForWithdrawals`** โ€” new shares to burn due to withdrawal request finalization #### Returns - **`withdrawals`** โ€” ETH amount allowed to be taken from the withdrawals vault - **`elRewards`** โ€” ETH amount allowed to be taken from the EL rewards vault - **`sharesFromWQToBurn`** โ€” amount of shares from Burner that should be burned due to WQ finalization - **`sharesToBurn`** โ€” amount of shares to be burnt (accounting for withdrawals finalization) ## Lever Methods ### setOracleReportLimits() Sets the new values for the limits list. :::note - Requires `ALL_LIMITS_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue(uint256 value, uint256 minAllowedValue, uint256 maxAllowedValue)` error when some value in the passed data out of the allowed range. See details of allowed value boundaries in the [Limits List](#limits-list) section. - Emits the corresponding `...Set` event for each changed limit value. - Emits `SecondOpinionOracleChanged(ISecondOpinionOracle indexed secondOpinionOracle)` in case of change for the second opinion oracle. ::: ```solidity function setOracleReportLimits(LimitsList calldata _limitsList, ISecondOpinionOracle _secondOpinionOracle) ``` #### Arguments - **`_limitsList`** โ€” new limits list values - **`_secondOpinionOracle`** โ€” new second opinion oracle value ### setExitedEthAmountPerDayLimit() Sets the new value for the `LimitsList.exitedEthAmountPerDayLimit`. The limit is applicable for the _**exited**_ ETH amount. :::note - Requires `EXITED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setExitedEthAmountPerDayLimit(uint256 _exitedEthAmountPerDayLimit) ``` #### Arguments - **`_exitedEthAmountPerDayLimit`** โ€” new `LimitsList.exitedEthAmountPerDayLimit` value ### setAppearedEthAmountPerDayLimit() Sets the new value for the `LimitsList.appearedEthAmountPerDayLimit`. The limit is applicable for the _**appeared**_ (activated) ETH amount. :::note - Requires `APPEARED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setAppearedEthAmountPerDayLimit(uint256 _appearedEthAmountPerDayLimit) ``` #### Arguments - **`_appearedEthAmountPerDayLimit`** โ€” new `LimitsList.appearedEthAmountPerDayLimit` value ### setConsolidationEthAmountPerDayLimit() Sets the new value for the `LimitsList.consolidationEthAmountPerDayLimit`. :::note - Requires `CONSOLIDATION_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setConsolidationEthAmountPerDayLimit(uint256 _consolidationEthAmountPerDayLimit) ``` #### Arguments - **`_consolidationEthAmountPerDayLimit`** โ€” new `LimitsList.consolidationEthAmountPerDayLimit` value ### setExitedValidatorEthAmountLimit() Sets the new value for the `LimitsList.exitedValidatorEthAmountLimit`. :::note - Requires `EXITED_VALIDATOR_ETH_AMOUNT_LIMIT_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setExitedValidatorEthAmountLimit(uint256 _exitedValidatorEthAmountLimit) ``` #### Arguments - **`_exitedValidatorEthAmountLimit`** โ€” new `LimitsList.exitedValidatorEthAmountLimit` value ### setExternalPendingBalanceCapEth() Sets the new value for the `LimitsList.externalPendingBalanceCapEth` โ€” the extra external pending balance cap tolerated above the Lido-funded pending balance. Stored in whole ETH units. :::note - Requires `EXTERNAL_PENDING_BALANCE_CAP_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setExternalPendingBalanceCapEth(uint256 _externalPendingBalanceCapEth) ``` #### Arguments - **`_externalPendingBalanceCapEth`** โ€” new `LimitsList.externalPendingBalanceCapEth` value ### setAnnualBalanceIncreaseBPLimit() Sets the new value for the `LimitsList.annualBalanceIncreaseBPLimit` variable. :::note - Requires `ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setAnnualBalanceIncreaseBPLimit(uint256 _annualBalanceIncreaseBPLimit) ``` #### Arguments - **`_annualBalanceIncreaseBPLimit`** โ€” new value for `LimitsList.annualBalanceIncreaseBPLimit` ### setSimulatedShareRateDeviationBPLimit() Sets the new value for the `LimitsList.simulatedShareRateDeviationBPLimit` variable. :::note - Requires `SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setSimulatedShareRateDeviationBPLimit(uint256 _simulatedShareRateDeviationBPLimit) ``` #### Arguments - **`_simulatedShareRateDeviationBPLimit`** โ€” new value for `LimitsList.simulatedShareRateDeviationBPLimit` ### setMaxBalanceExitRequestedPerReportInEth() Sets the new value for the `LimitsList.maxBalanceExitRequestedPerReportInEth`. :::note - Requires `MAX_BALANCE_EXIT_REQUESTED_PER_REPORT_IN_ETH_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxBalanceExitRequestedPerReportInEth(uint256 _maxBalanceExitRequestedPerReportInEth) ``` #### Arguments - **`_maxBalanceExitRequestedPerReportInEth`** โ€” new value for `LimitsList.maxBalanceExitRequestedPerReportInEth` ### setMaxEffectiveBalanceWeightWCType01() Sets the new max effective balance equivalent weight in ETH for validators with the `0x01` type withdrawal credentials. :::note - Requires `MAX_EFFECTIVE_BALANCE_WEIGHTS_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxEffectiveBalanceWeightWCType01(uint256 _maxEffectiveBalanceWeightWCType01) ``` #### Arguments - **`_maxEffectiveBalanceWeightWCType01`** โ€” new value for `LimitsList.maxEffectiveBalanceWeightWCType01` ### setMaxEffectiveBalanceWeightWCType02() Sets the new max effective balance equivalent weight in ETH for validators with the `0x02` type withdrawal credentials. :::note - Requires `MAX_EFFECTIVE_BALANCE_WEIGHTS_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxEffectiveBalanceWeightWCType02(uint256 _maxEffectiveBalanceWeightWCType02) ``` #### Arguments - **`_maxEffectiveBalanceWeightWCType02`** โ€” new value for `LimitsList.maxEffectiveBalanceWeightWCType02` ### setRequestTimestampMargin() Sets the new value for the `LimitsList.requestTimestampMargin` variable. :::note - Requires `REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setRequestTimestampMargin(uint256 _requestTimestampMargin) ``` #### Arguments - **`_requestTimestampMargin`** โ€” new value for `LimitsList.requestTimestampMargin` ### setMaxPositiveTokenRebase() Sets the new value for the `LimitsList.maxPositiveTokenRebase` variable. :::note - Requires `MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxPositiveTokenRebase(uint256 _maxPositiveTokenRebase) ``` #### Arguments - **`_maxPositiveTokenRebase`** โ€” new value for `LimitsList.maxPositiveTokenRebase` ### setMaxItemsPerExtraDataTransaction() Sets the new value for the `LimitsList.maxItemsPerExtraDataTransaction` variable. :::note - Requires `MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxItemsPerExtraDataTransaction(uint256 _maxItemsPerExtraDataTransaction) ``` #### Arguments - **`_maxItemsPerExtraDataTransaction`** โ€” new value for `LimitsList.maxItemsPerExtraDataTransaction` ### setMaxNodeOperatorsPerExtraDataItem() Sets the new value for the `LimitsList.maxNodeOperatorsPerExtraDataItem` variable. :::note - Requires `MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxNodeOperatorsPerExtraDataItem(uint256 _maxNodeOperatorsPerExtraDataItem) ``` #### Arguments - **`_maxNodeOperatorsPerExtraDataItem`** โ€” new value for `LimitsList.maxNodeOperatorsPerExtraDataItem` ### setSecondOpinionOracleAndCLBalanceUpperMargin() Sets the new value for the Second Opinion Oracle and `LimitsList.clBalanceOraclesErrorUpperBPLimit` variable. :::note - Requires `SECOND_OPINION_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. - Emits `SecondOpinionOracleChanged(ISecondOpinionOracle indexed secondOpinionOracle)` in case of change for the second opinion oracle. ::: ```solidity function setSecondOpinionOracleAndCLBalanceUpperMargin(ISecondOpinionOracle _secondOpinionOracle, uint256 _clBalanceOraclesErrorUpperBPLimit) ``` #### Arguments - **`_secondOpinionOracle`** โ€” new value for Second Opinion Oracle (zero address disables the Second Opinion Oracle) - **`_clBalanceOraclesErrorUpperBPLimit`** โ€” new value for `LimitsList.clBalanceOraclesErrorUpperBPLimit` ### setMaxCLBalanceDecreaseBP() Sets the new value for the `LimitsList.maxCLBalanceDecreaseBP` variable โ€” the max allowed CL balance decrease over the 36-day sliding window, in basis points (e.g. `360` = 3.6%). :::note - Requires `MAX_CL_BALANCE_DECREASE_MANAGER_ROLE` to be granted to the caller. - Reverts with `IncorrectLimitValue()` error when the passed value is out of the allowed range. See [Limits List](#limits-list) section for details. ::: ```solidity function setMaxCLBalanceDecreaseBP(uint256 _maxCLBalanceDecreaseBP) ``` #### Arguments - **`_maxCLBalanceDecreaseBP`** โ€” new value for `LimitsList.maxCLBalanceDecreaseBP` ## Permissions ### ALL_LIMITS_MANAGER_ROLE() ```solidity bytes32 public constant ALL_LIMITS_MANAGER_ROLE = keccak256("ALL_LIMITS_MANAGER_ROLE") ``` Granting this role allows updating **ANY** value of the Limits List. See [`setOracleReportLimits()`](#setoraclereportlimits) method. **Grant this role with caution and give preference to the granular roles described below.** ### EXITED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE() Granting this role allows updating the `exitedEthAmountPerDayLimit` value of the [Limits List](#limits-list). See the [`setExitedEthAmountPerDayLimit()`](#setexitedethamountperdaylimit) method. ```solidity bytes32 public constant EXITED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE = keccak256("EXITED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE"); ``` ### APPEARED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE() Granting this role allows updating the `appearedEthAmountPerDayLimit` value of the [Limits List](#limits-list). See the [`setAppearedEthAmountPerDayLimit()`](#setappearedethamountperdaylimit) method. ```solidity bytes32 public constant APPEARED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE = keccak256("APPEARED_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE"); ``` ### CONSOLIDATION_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE() Granting this role allows updating the `consolidationEthAmountPerDayLimit` value of the [Limits List](#limits-list). See the [`setConsolidationEthAmountPerDayLimit()`](#setconsolidationethamountperdaylimit) method. ```solidity bytes32 public constant CONSOLIDATION_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE = keccak256("CONSOLIDATION_ETH_AMOUNT_PER_DAY_LIMIT_MANAGER_ROLE"); ``` ### EXITED_VALIDATOR_ETH_AMOUNT_LIMIT_MANAGER_ROLE() Granting this role allows updating the `exitedValidatorEthAmountLimit` value of the [Limits List](#limits-list). See the [`setExitedValidatorEthAmountLimit()`](#setexitedvalidatorethamountlimit) method. ```solidity bytes32 public constant EXITED_VALIDATOR_ETH_AMOUNT_LIMIT_MANAGER_ROLE = keccak256("EXITED_VALIDATOR_ETH_AMOUNT_LIMIT_MANAGER_ROLE"); ``` ### EXTERNAL_PENDING_BALANCE_CAP_MANAGER_ROLE() Granting this role allows updating the `externalPendingBalanceCapEth` value of the [Limits List](#limits-list). See the [`setExternalPendingBalanceCapEth()`](#setexternalpendingbalancecapeth) method. ```solidity bytes32 public constant EXTERNAL_PENDING_BALANCE_CAP_MANAGER_ROLE = keccak256("EXTERNAL_PENDING_BALANCE_CAP_MANAGER_ROLE"); ``` ### ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE() Granting this role allows updating the `annualBalanceIncreaseBPLimit` value of the [Limits List](#limits-list). See the [`setAnnualBalanceIncreaseBPLimit()`](#setannualbalanceincreasebplimit) method. ```solidity bytes32 public constant ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE = keccak256("ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE") ``` ### SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE() Granting this role allows updating the `simulatedShareRateDeviationBPLimit` value of the [Limits List](#limits-list). See the [`setSimulatedShareRateDeviationBPLimit()`](#setsimulatedshareratedeviationbplimit) method. ```solidity bytes32 public constant SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE = keccak256("SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE") ``` ### MAX_BALANCE_EXIT_REQUESTED_PER_REPORT_IN_ETH_ROLE() Granting this role allows updating the `maxBalanceExitRequestedPerReportInEth` value of the [Limits List](#limits-list). See the [`setMaxBalanceExitRequestedPerReportInEth()`](#setmaxbalanceexitrequestedperreportineth) method. ```solidity bytes32 public constant MAX_BALANCE_EXIT_REQUESTED_PER_REPORT_IN_ETH_ROLE = keccak256("MAX_BALANCE_EXIT_REQUESTED_PER_REPORT_IN_ETH_ROLE") ``` ### MAX_EFFECTIVE_BALANCE_WEIGHTS_MANAGER_ROLE() Granting this role allows updating the `maxEffectiveBalanceWeightWCType01` and `maxEffectiveBalanceWeightWCType02` values of the [Limits List](#limits-list). See the [`setMaxEffectiveBalanceWeightWCType01()`](#setmaxeffectivebalanceweightwctype01) and [`setMaxEffectiveBalanceWeightWCType02()`](#setmaxeffectivebalanceweightwctype02) methods. ```solidity bytes32 public constant MAX_EFFECTIVE_BALANCE_WEIGHTS_MANAGER_ROLE = keccak256("MAX_EFFECTIVE_BALANCE_WEIGHTS_MANAGER_ROLE") ``` ### MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION_ROLE() Granting this role allows updating the `maxItemsPerExtraDataTransaction` value of the [Limits List](#limits-list). See the [`setMaxItemsPerExtraDataTransaction()`](#setmaxitemsperextradatatransaction) method. ```solidity bytes32 public constant MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION_ROLE = keccak256("MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION_ROLE"); ``` ### MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_ROLE() Granting this role allows updating the `maxNodeOperatorsPerExtraDataItem` value of the [Limits List](#limits-list). See the [`setMaxNodeOperatorsPerExtraDataItem()`](#setmaxnodeoperatorsperextradataitem) method. ```solidity bytes32 public constant MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_ROLE = keccak256("MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_ROLE"); ``` ### REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE() Granting this role allows updating the `requestTimestampMargin` value of the [Limits List](#limits-list). See the [`setRequestTimestampMargin()`](#setrequesttimestampmargin) method. ```solidity bytes32 public constant REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE = keccak256("REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE") ``` ### MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE() Granting this role allows updating the `maxPositiveTokenRebase` value of the [Limits List](#limits-list). See the [`setMaxPositiveTokenRebase()`](#setmaxpositivetokenrebase) method. ```solidity bytes32 public constant MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE = keccak256("MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE") ``` ### SECOND_OPINION_MANAGER_ROLE() Granting this role allows updating the Second Opinion Oracle and `clBalanceOraclesErrorUpperBPLimit` value of the [Limits List](#limits-list). See the [`setSecondOpinionOracleAndCLBalanceUpperMargin()`](#setsecondopinionoracleandclbalanceuppermargin) method. ```solidity bytes32 public constant SECOND_OPINION_MANAGER_ROLE = keccak256("SECOND_OPINION_MANAGER_ROLE") ``` ### MAX_CL_BALANCE_DECREASE_MANAGER_ROLE() Granting this role allows updating the `maxCLBalanceDecreaseBP` value of the [Limits List](#limits-list). See the [`setMaxCLBalanceDecreaseBP()`](#setmaxclbalancedecreasebp) method. ```solidity bytes32 public constant MAX_CL_BALANCE_DECREASE_MANAGER_ROLE = keccak256("MAX_CL_BALANCE_DECREASE_MANAGER_ROLE") ``` ## Events ### ExitedEthAmountPerDayLimitSet() Emits whenever the value of the `LimitsList.exitedEthAmountPerDayLimit` value is changed. ```solidity event ExitedEthAmountPerDayLimitSet(uint256 exitedEthAmountPerDayLimit); ``` #### Arguments - **`exitedEthAmountPerDayLimit`** โ€” new value of the `LimitsList.exitedEthAmountPerDayLimit` ### AppearedEthAmountPerDayLimitSet() Emits whenever the value of the `LimitsList.appearedEthAmountPerDayLimit` value is changed. ```solidity event AppearedEthAmountPerDayLimitSet(uint256 appearedEthAmountPerDayLimit); ``` #### Arguments - **`appearedEthAmountPerDayLimit`** โ€” new value of the `LimitsList.appearedEthAmountPerDayLimit` ### ConsolidationEthAmountPerDayLimitSet() Emits whenever the value of the `LimitsList.consolidationEthAmountPerDayLimit` value is changed. ```solidity event ConsolidationEthAmountPerDayLimitSet(uint256 consolidationEthAmountPerDayLimit); ``` #### Arguments - **`consolidationEthAmountPerDayLimit`** โ€” new value of the `LimitsList.consolidationEthAmountPerDayLimit` ### ExitedValidatorEthAmountLimitSet() Emits whenever the value of the `LimitsList.exitedValidatorEthAmountLimit` value is changed. ```solidity event ExitedValidatorEthAmountLimitSet(uint256 exitedValidatorEthAmountLimit); ``` #### Arguments - **`exitedValidatorEthAmountLimit`** โ€” new value of the `LimitsList.exitedValidatorEthAmountLimit` ### ExternalPendingBalanceCapEthSet() Emits whenever the value of the `LimitsList.externalPendingBalanceCapEth` value is changed. ```solidity event ExternalPendingBalanceCapEthSet(uint256 externalPendingBalanceCapEth); ``` #### Arguments - **`externalPendingBalanceCapEth`** โ€” new value of the `LimitsList.externalPendingBalanceCapEth` ### SecondOpinionOracleChanged() Emits whenever the Second Opinion Oracle address is changed. ```solidity event SecondOpinionOracleChanged(ISecondOpinionOracle indexed secondOpinionOracle); ``` #### Arguments - **`secondOpinionOracle`** โ€” new address of the Second Opinion Oracle ### AnnualBalanceIncreaseBPLimitSet() Emits whenever the value of the `LimitsList.annualBalanceIncreaseBPLimit` value is changed. ```solidity event AnnualBalanceIncreaseBPLimitSet(uint256 annualBalanceIncreaseBPLimit); ``` #### Arguments - **`annualBalanceIncreaseBPLimit`** โ€” new value of the `LimitsList.annualBalanceIncreaseBPLimit` ### SimulatedShareRateDeviationBPLimitSet() Emits whenever the value of the `LimitsList.simulatedShareRateDeviationBPLimit` value is changed. ```solidity event SimulatedShareRateDeviationBPLimitSet(uint256 simulatedShareRateDeviationBPLimit); ``` #### Arguments - **`simulatedShareRateDeviationBPLimit`** โ€” new value of the `LimitsList.simulatedShareRateDeviationBPLimit` ### MaxPositiveTokenRebaseSet() Emits whenever the value of the `LimitsList.maxPositiveTokenRebase` value is changed. ```solidity event MaxPositiveTokenRebaseSet(uint256 maxPositiveTokenRebase); ``` #### Arguments - **`maxPositiveTokenRebase`** โ€” new value of the `LimitsList.maxPositiveTokenRebase` ### MaxBalanceExitRequestedPerReportInEthSet() Emits whenever the value of the `LimitsList.maxBalanceExitRequestedPerReportInEth` value is changed. ```solidity event MaxBalanceExitRequestedPerReportInEthSet(uint256 maxBalanceExitRequestedPerReportInEth); ``` #### Arguments - **`maxBalanceExitRequestedPerReportInEth`** โ€” new value of the `LimitsList.maxBalanceExitRequestedPerReportInEth` ### MaxEffectiveBalanceWeightWCType01Set() Emits whenever the value of the `LimitsList.maxEffectiveBalanceWeightWCType01` value is changed. ```solidity event MaxEffectiveBalanceWeightWCType01Set(uint256 maxEffectiveBalanceWeightWCType01); ``` #### Arguments - **`maxEffectiveBalanceWeightWCType01`** โ€” new value of the `LimitsList.maxEffectiveBalanceWeightWCType01` ### MaxEffectiveBalanceWeightWCType02Set() Emits whenever the value of the `LimitsList.maxEffectiveBalanceWeightWCType02` value is changed. ```solidity event MaxEffectiveBalanceWeightWCType02Set(uint256 maxEffectiveBalanceWeightWCType02); ``` #### Arguments - **`maxEffectiveBalanceWeightWCType02`** โ€” new value of the `LimitsList.maxEffectiveBalanceWeightWCType02` ### MaxItemsPerExtraDataTransactionSet() Emits whenever the value of the `LimitsList.maxItemsPerExtraDataTransaction` value is changed. ```solidity event MaxItemsPerExtraDataTransactionSet(uint256 maxItemsPerExtraDataTransaction); ``` #### Arguments - **`maxItemsPerExtraDataTransaction`** โ€” new value of the `LimitsList.maxItemsPerExtraDataTransaction` ### MaxNodeOperatorsPerExtraDataItemSet() Emits whenever the value of the `LimitsList.maxNodeOperatorsPerExtraDataItem` value is changed. ```solidity event MaxNodeOperatorsPerExtraDataItemSet(uint256 maxNodeOperatorsPerExtraDataItem); ``` #### Arguments - **`maxNodeOperatorsPerExtraDataItem`** โ€” new value of the `LimitsList.maxNodeOperatorsPerExtraDataItem` ### RequestTimestampMarginSet() Emits whenever the value of the `LimitsList.requestTimestampMargin` value is changed. ```solidity event RequestTimestampMarginSet(uint256 requestTimestampMargin); ``` #### Arguments - **`requestTimestampMargin`** โ€” new value of the `LimitsList.requestTimestampMargin` ### MaxCLBalanceDecreaseBPSet() Emits whenever the value of the `LimitsList.maxCLBalanceDecreaseBP` value is changed. ```solidity event MaxCLBalanceDecreaseBPSet(uint256 maxCLBalanceDecreaseBP); ``` #### Arguments - **`maxCLBalanceDecreaseBP`** โ€” new value of the `LimitsList.maxCLBalanceDecreaseBP` ### CLBalanceOraclesErrorUpperBPLimitSet() Emits whenever the value of the `LimitsList.clBalanceOraclesErrorUpperBPLimit` value is changed. ```solidity event CLBalanceOraclesErrorUpperBPLimitSet(uint256 clBalanceOraclesErrorUpperBPLimit); ``` #### Arguments - **`clBalanceOraclesErrorUpperBPLimit`** โ€” new value of the `LimitsList.clBalanceOraclesErrorUpperBPLimit` ### NegativeCLRebaseConfirmed() Emits whenever the checkAccountingOracleReport() finished with negative CL rebase check successfully with checking the Second Opinion Oracle. ```solidity event NegativeCLRebaseConfirmed(uint256 refSlot, uint256 clBalanceWei, uint256 withdrawalVaultBalance); ``` #### Arguments - **`refSlot`** โ€” the reference slot for the report checked. - **`clBalanceWei`** โ€” the reported CL validators balance (excluding pending deposits), in wei. - **`withdrawalVaultBalance`** โ€” balance of the withdrawal vault. ### NegativeCLRebaseAccepted() Emits whenever the checkAccountingOracleReport() finished with negative CL rebase check successfully without checking the Second Opinion Oracle. ```solidity event NegativeCLRebaseAccepted(uint256 refSlot, uint256 clTotalBalance, uint256 clBalanceDecrease, uint256 maxAllowedDecrease); ``` #### Arguments - **`refSlot`** โ€” the reference slot for the report checked. - **`clTotalBalance`** โ€” the total Consensus Layer balance (CL validators balance plus CL pending balance). - **`clBalanceDecrease`** โ€” the decrease of Consensus Layer balance over the sliding window. - **`maxAllowedDecrease`** โ€” the maximum accepted CL balance decrease without second opinion. ### BaselineSnapshotMigrated() Emits on the successful [`migrateBaselineSnapshot()`](#migratebaselinesnapshot) call. ```solidity event BaselineSnapshotMigrated(uint256 clBalance, uint256 clWithdrawals); ``` #### Arguments - **`clBalance`** โ€” the CL balance seeded as the baseline snapshot. - **`clWithdrawals`** โ€” the withdrawal vault balance recorded as CL withdrawals at the migration moment. --- # OssifiableProxy `OssifiableProxy` is an ERC-1967 proxy used for non-Aragon upgradeable contract deployments. Its admin can permanently disable upgrades by setting the proxy admin to the zero address. There are several slightly different variants of the `OssifiableProxy` contract. They use different versions of the OpenZeppelin libraries, resulting in slightly different interfaces, but their core functionality remains the same. Every contract listed on this page is deployed behind one of the Lido variants described below; none of them use the vanilla OpenZeppelin `ERC1967Proxy` directly. ## Proxy variants ### Core variant - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/proxy/OssifiableProxy.sol) Defined in the [core](https://github.com/lidofinance/core) repository. Written in Solidity 0.8.9 on top of the OpenZeppelin v4.4 `ERC1967Proxy`. The upgrade-and-call function takes three arguments: `proxy__upgradeToAndCall(address newImplementation_, bytes setupCalldata_, bool forceCall_)`. ### Staking modules variant - [Source code](https://github.com/lidofinance/staking-modules/blob/v3.0/src/lib/proxy/OssifiableProxy.sol) Defined in the [staking-modules](https://github.com/lidofinance/staking-modules) repository. Written in Solidity 0.8.33 on top of the OpenZeppelin v5 `ERC1967Proxy`. Following the OpenZeppelin v5 interface changes, the upgrade-and-call function takes two arguments โ€” `proxy__upgradeToAndCall(address newImplementation_, bytes setupCalldata_)`. ## Core contracts All core protocol contracts are deployed behind the [core variant](#core-variant): - [LidoLocator](/contracts/lido-locator) - [Accounting](/contracts/accounting) - [StakingRouter](/contracts/staking-router) - [WithdrawalQueueERC721](/contracts/withdrawal-queue-erc721) - [Burner](/contracts/burner) - [TopUpGateway](https://etherscan.io/address/0x3FC2C71579D80790Aaa3fc7Be8B66ac39dC57374) - [VaultHub](/contracts/vault-hub) - [PredepositGuarantee](/contracts/predeposit-guarantee) - [OperatorGrid](/contracts/operator-grid) - [ConsolidationMigrator](https://etherscan.io/address/0x9Dc70b5A4f4F5E4AF9058C983D560564F031f1D7) - [ConsolidationBus](https://etherscan.io/address/0xd907CE33B4Be423823d1CFFe80BD147E8b8554C8) - [AccountingOracle](/contracts/accounting-oracle) - [ValidatorsExitBusOracle](/contracts/validators-exit-bus-oracle) - [LazyOracle](/contracts/lazy-oracle) ## Staking module contracts ### Community Staking Module The following contracts are deployed behind the [staking modules variant](#staking-modules-variant): - [CSModule](/staking-modules/csm/contracts/CSModule) - [Accounting](/staking-modules/csm/contracts/Accounting) - [ParametersRegistry](/staking-modules/csm/contracts/ParametersRegistry) - [FeeDistributor](/staking-modules/csm/contracts/FeeDistributor) - [FeeOracle](/staking-modules/csm/contracts/FeeOracle) - [ValidatorStrikes](/staking-modules/csm/contracts/ValidatorStrikes) - [ExitPenalties](/staking-modules/csm/contracts/ExitPenalties) #### Gates - [Identified Community Stakers Gate](/staking-modules/csm/contracts/VettedGate) โ€” [staking modules variant](#staking-modules-variant) - [Identified DVT Cluster Gate](/staking-modules/csm/contracts/VettedGate) โ€” [staking modules variant](#staking-modules-variant), as it was deployed later than the other CSM proxies ### Curated Module v2 All Curated Module v2 contracts, including the gates, are deployed behind the [staking modules variant](#staking-modules-variant): - [CuratedModule](/staking-modules/cm-v2/contracts/CuratedModule) - [MetaRegistry](/staking-modules/cm-v2/contracts/MetaRegistry) - [Accounting](/staking-modules/cm-v2/contracts/Accounting) - [ParametersRegistry](/staking-modules/cm-v2/contracts/ParametersRegistry) - [FeeDistributor](/staking-modules/cm-v2/contracts/FeeDistributor) - [FeeOracle](/staking-modules/cm-v2/contracts/FeeOracle) - [ValidatorStrikes](/staking-modules/cm-v2/contracts/ValidatorStrikes) - [ExitPenalties](/staking-modules/cm-v2/contracts/ExitPenalties) #### Gates - [Professional Operator Gate](/staking-modules/cm-v2/contracts/CuratedGate) - [Professional Trusted Operator Gate](/staking-modules/cm-v2/contracts/CuratedGate) - [Public Good Operator Gate](/staking-modules/cm-v2/contracts/CuratedGate) - [Decentralization Operator Gate](/staking-modules/cm-v2/contracts/CuratedGate) - [Extra Effort Operator Gate](/staking-modules/cm-v2/contracts/CuratedGate) - [Intra-Operator DVT Cluster Gate](/staking-modules/cm-v2/contracts/CuratedGate) - [Intra-Operator DVT Cluster Plus Gate](/staking-modules/cm-v2/contracts/CuratedGate) --- # PredepositGuarantee - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/predeposit_guarantee/PredepositGuarantee.sol) - [Deployed contract](https://etherscan.io/address/0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3) PredepositGuarantee (PDG) mitigates deposit frontrunning by requiring a node operator guarantee and validator withdrawal credentials proofs (EIP-4788) before activating staged deposits. ## What is PredepositGuarantee? PDG enables trust-minimized deposits for stVaults: - node operators post a guarantee (1 ETH per predeposit) - vault owners stage 31 ETH per validator - PDG verifies validator existence and withdrawal credentials via beacon root proofs - PDG unlocks guarantees once activation is proven ### Trust assumptions There are mutual trust assumptions in PDG: - **NO โ†” Guarantor**: Guards prevent mistakes but cannot fully prevent misbehavior where NOs can access guarantor-provided ether - **NO โ†” Depositor**: The designated depositor acts on behalf of the node operator ### On-chain BLS verification Predeposit operations require valid BLS12-381 signatures verified on-chain using [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) precompiles. This cryptographic verification ensures: - The predeposit is legitimate and properly signed - The validator will appear on the consensus layer with correct withdrawal credentials - Frontrunning attacks cannot substitute different validator keys ## How it works 1. Guarantor tops up the node operator balance. 2. Depositor submits a 1 ETH predeposit for a validator with BLS signature verification. 3. Depositor proves validator inclusion to PDG via Merkle proof against beacon block root. 4. PDG activates the validator by depositing the remaining 31 ETH from staged balance. ```mermaid sequenceDiagram participant G as Guarantor participant D as Depositor participant P as PredepositGuarantee participant V as StakingVault participant BC as Beacon Chain G->>P: topUpNodeOperatorBalance() D->>P: predeposit(vault, deposits, depositsY) P->>V: stage(31 ETH) P->>BC: deposit 1 ETH D->>P: proveWCAndActivate(witness) P->>V: depositFromStaged(31 ETH) P->>BC: validator activated P-->>G: unlock guarantee ``` See the [PDG guide](/run-on-lido/stvaults/tech-documentation/pdg) for step-by-step flows. ## Constants | Constant | Value | Description | | --------------------------- | ---------- | ---------------------------------------- | | `MIN_SUPPORTED_WC_VERSION` | 0x01 | Minimum withdrawal credentials version | | `MAX_SUPPORTED_WC_VERSION` | 0x02 | Maximum withdrawal credentials version | | `PREDEPOSIT_AMOUNT` | 1 ether | Amount deposited with each predeposit | | `ACTIVATION_DEPOSIT_AMOUNT` | 31 ether | Amount deposited to activate a validator | | `MAX_TOPUP_AMOUNT` | 2016 ether | Maximum top-up amount (2048 - 31 - 1) | | `DEPOSIT_DOMAIN` | (computed) | Chain-specific deposit domain | ## Structs ### ValidatorStage (enum) Validator lifecycle stages in PDG: ```solidity enum ValidatorStage { NONE, // Initial stage, validator unknown to PDG PREDEPOSITED, // 1 ETH predeposit made, guarantee locked PROVEN, // Validator WC proven via beacon proof ACTIVATED, // 31 ETH activation deposit made COMPENSATED // Frontrun detected, guarantee compensated to vault } ``` ### ValidatorStatus Status of a validator in PDG: ```solidity struct ValidatorStatus { ValidatorStage stage; // Current lifecycle stage IStakingVault stakingVault; // Vault the validator belongs to address nodeOperator; // Node operator responsible } ``` ### ValidatorWitness Proof data for validator verification against beacon block root: ```solidity struct ValidatorWitness { bytes32[] proof; // Merkle proof from parent(pubkey,wc) to beacon block root bytes pubkey; // Validator public key uint256 validatorIndex; // Validator index on beacon chain uint64 childBlockTimestamp;// Timestamp of EL block with beacon root uint64 slot; // Beacon slot for the proof uint64 proposerIndex; // Beacon block proposer index } ``` ### NodeOperatorBalance Node operator guarantee balance (fits in single slot): ```solidity struct NodeOperatorBalance { uint128 total; // Total guarantee balance uint128 locked; // Locked for pending predeposits } ``` ### ValidatorTopUp Parameters for topping up existing validators: ```solidity struct ValidatorTopUp { bytes pubkey; // Public key of validator to top up uint256 amount; // Amount of ether to deposit (max 2016 ETH) } ``` ## View methods ### nodeOperatorBalance(address \_nodeOperator) ```solidity function nodeOperatorBalance(address _nodeOperator) external view returns (NodeOperatorBalance memory) ``` Returns node operator total and locked guarantee balances. ### unlockedBalance(address \_nodeOperator) ```solidity function unlockedBalance(address _nodeOperator) external view returns (uint256 unlocked) ``` Returns unlocked guarantee balance (total - locked). ### nodeOperatorGuarantor(address \_nodeOperator) ```solidity function nodeOperatorGuarantor(address _nodeOperator) external view returns (address) ``` Returns guarantor for the node operator. Returns the node operator address if they are self-guarantor. ### nodeOperatorDepositor(address \_nodeOperator) ```solidity function nodeOperatorDepositor(address _nodeOperator) external view returns (address) ``` Returns depositor for the node operator. Returns the node operator address if no depositor is set. ### claimableRefund(address \_guarantor) ```solidity function claimableRefund(address _guarantor) external view returns (uint256) ``` Returns claimable refund for a guarantor (from changing guarantor with balance). ### validatorStatus(bytes \_validatorPubkey) ```solidity function validatorStatus(bytes calldata _validatorPubkey) external view returns (ValidatorStatus memory) ``` Returns PDG status for a validator by pubkey. ### pendingActivations(IStakingVault \_vault) ```solidity function pendingActivations(IStakingVault _vault) external view returns (uint256) ``` Returns number of validators in PREDEPOSITED or PROVEN stages awaiting activation. ### validatePubKeyWCProof(ValidatorWitness \_witness, bytes32 \_withdrawalCredentials) ```solidity function validatePubKeyWCProof( ValidatorWitness calldata _witness, bytes32 _withdrawalCredentials ) external view ``` Validates a Merkle proof of validator pubkey and withdrawal credentials against beacon block root. Reverts with `InvalidProof` if invalid. ### verifyDepositMessage(...) ```solidity function verifyDepositMessage( IStakingVault.Deposit calldata _deposit, BLS12_381.DepositY calldata _depositsY, bytes32 _withdrawalCredentials ) public view ``` Verifies deposit message BLS signature using EIP-2537 precompiles. Reverts with `InvalidSignature` if invalid. ## Methods ### initialize(address \_defaultAdmin) ```solidity function initialize(address _defaultAdmin) external initializer ``` Initializes PDG with admin role. ### topUpNodeOperatorBalance(address \_nodeOperator) ```solidity function topUpNodeOperatorBalance(address _nodeOperator) external payable whenResumed ``` Tops up guarantee balance for a node operator. Only callable by the node operator's guarantor. Amount must be a multiple of 1 ETH. ### withdrawNodeOperatorBalance(address \_nodeOperator, uint256 \_amount, address \_recipient) ```solidity function withdrawNodeOperatorBalance( address _nodeOperator, uint256 _amount, address _recipient ) external whenResumed ``` Withdraws unlocked guarantee balance. Only callable by the node operator's guarantor. Amount must be a multiple of 1 ETH. ### setNodeOperatorGuarantor(address \_newGuarantor) ```solidity function setNodeOperatorGuarantor(address _newGuarantor) external whenResumed ``` Sets the guarantor for the calling node operator. If there's existing balance with a different guarantor, it becomes claimable by the previous guarantor. Reverts if locked balance is non-zero. ### setNodeOperatorDepositor(address \_newDepositor) ```solidity function setNodeOperatorDepositor(address _newDepositor) external whenResumed ``` Sets the depositor for the calling node operator. ### claimGuarantorRefund(address \_recipient) ```solidity function claimGuarantorRefund(address _recipient) external whenResumed returns (uint256 claimedEther) ``` Claims a guarantor refund (from previous NO relationship). ### predeposit(...) ```solidity function predeposit( IStakingVault _stakingVault, IStakingVault.Deposit[] calldata _deposits, BLS12_381.DepositY[] calldata _depositsY ) external payable whenResumed ``` Performs 1 ETH predeposits for validators. Requires: - Caller is the node operator's depositor - BLS signatures are valid (verified via `_depositsY`) - Sufficient unlocked guarantee balance - Deposit amounts are exactly `PREDEPOSIT_AMOUNT` Optionally accepts msg.value (multiples of 1 ETH) to top up balance if NO is self-guarantor. **State transition:** NONE โ†’ PREDEPOSITED ### proveWCAndActivate(ValidatorWitness \_witness) ```solidity function proveWCAndActivate(ValidatorWitness calldata _witness) external whenResumed ``` Proves validator withdrawal credentials and activates if possible. Unlocks the guarantee. If activation fails (vault disconnected), validator moves to PROVEN state for later activation. **State transition:** PREDEPOSITED โ†’ PROVEN [โ†’ ACTIVATED] ### activateValidator(bytes \_pubkey) ```solidity function activateValidator(bytes calldata _pubkey) external whenResumed ``` Activates a previously proven validator by depositing 31 ETH from staged balance. **State transition:** PROVEN โ†’ ACTIVATED ### proveUnknownValidator(...) ```solidity function proveUnknownValidator( ValidatorWitness calldata _witness, IStakingVault _stakingVault ) external whenResumed ``` Registers a validator that was deposited outside PDG (side-deposited). Only callable by vault owner. Validator must be eligible for activation (not in far-future epoch). **State transition:** NONE โ†’ ACTIVATED ### proveInvalidValidatorWC(...) ```solidity function proveInvalidValidatorWC( ValidatorWitness calldata _witness, bytes32 _invalidWithdrawalCredentials ) external whenResumed ``` Proves that a predeposit was frontrun with invalid withdrawal credentials. Compensates the vault from the locked guarantee. **State transition:** PREDEPOSITED โ†’ COMPENSATED ### topUpExistingValidators(ValidatorTopUp[] \_topUps) ```solidity function topUpExistingValidators(ValidatorTopUp[] calldata _topUps) external whenResumed ``` Tops up existing activated validators from their respective vault balances. Only callable by the node operator's depositor. Maximum top-up per validator is 2016 ETH. ### proveWCActivateAndTopUpValidators(...) ```solidity function proveWCActivateAndTopUpValidators( ValidatorWitness[] calldata _witnesses, uint256[] calldata _amounts ) external whenResumed ``` Batch operation to prove, activate, and optionally top up multiple validators. Handles validators in PREDEPOSITED, PROVEN, or ACTIVATED states. Top-up amounts require caller to be the depositor. **State transitions:** [PREDEPOSITED โ†’] [PROVEN โ†’] ACTIVATED ## Pausable PDG inherits from `PausableUntilWithRoles`. All state-changing methods require the contract to be resumed (`whenResumed` modifier). The contract is initialized in a paused state. ## Related - [StakingVault](/contracts/staking-vault) - [VaultHub](/contracts/vault-hub) - [Dashboard](/contracts/dashboard) - [PDG guide](/run-on-lido/stvaults/tech-documentation/pdg) --- # ReserveFund - [Source code](https://github.com/lidofinance/insurance-fund/blob/main/contracts/InsuranceFund.sol) - [Deployed contract](https://etherscan.io/address/0x8B3f33234ABD88493c0Cd28De33D583B70beDe35) - [LIP-18](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-18.md) The Lido Reserve Fund is a vault contract that serves as a simple transparent store for funds allocated for self-cover purposes. ## Mechanics The Reserve Fund is a simple vault that inherits OpenZeppelin's [Ownable](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/access/Ownable.sol) and allows the owner to transfer ether, ERC20, ERC721, ERC1155 tokens from the contract. The owner, which will the Lido DAO Agent, can transfer ownership to another entity with an exception of [zero address](https://etherscan.io/address/0x0000000000000000000000000000000000000000). ## View methods ### owner() Returns the current `owner`. ```solidity function owner() public view returns (address); ``` ### renounceOwnership() Reverts always. ```solidity function renounceOwnership() public pure override; ``` ## Methods ### transferERC1155() Transfer a single ERC1155 token with the specified id in the specified amount to an entity from the contract balance. A contract recipient must implement `ERC1155TokenReceiver` in accordance to [EIP-1155](https://eips.ethereum.org/EIPS/eip-1155) in order to safely receive tokens. - reverts if `msg.sender` is not `owner`; - reverts if `_recipient` is zero address; - reverts if the contract balance is insufficient; - emits `ERC1155Transferred(address indexed _token, address indexed _recipient, uint256 _tokenId, bytes _data)`. ```solidity function transferERC1155(address _token, address _recipient, uint256 _tokenId, uint256 _amount, bytes calldata _data) external; ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ---------------- | | `_token` | `address` | an ERC1155 token | | `_recipient` | `address` | recipient entity | | `_tokenId` | `uint256` | token identifier | | `_amount` | `uint256` | transfer amount | | `_data` | `bytes` | byte sequence for `onERC1155Received` hook | :::info Note: `transferERC1155` does not support multi-token batch transfers. ::: ### transferERC20() Transfer an ERC20 token to an entity in the specified amount from the contract balance. - reverts if `msg.sender` is not `owner`; - reverts if `_recipient` is zero address; - reverts if the contract balance is insufficient; - emits `ERC20Transferred(address indexed _token, address indexed _recipient, uint256 _amount)`. ```solidity function transferERC20(address _token, address _recipient, uint256 _amount) external; ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ---------------- | | `_token` | `address` | an ERC20 token | | `_recipient` | `address` | recipient entity | | `_amount` | `uint256` | transfer amount | ### transferERC721() Transfer a single ERC721 token with the specified id to an entity from the contract balance. A contract recipient must implement `ERC721TokenReceiver` in accordance to [EIP-721](https://eips.ethereum.org/EIPS/eip-721) in order to safely receive tokens. - reverts if `msg.sender` is not `owner`; - reverts if `_recipient` is zero address; - emits `ERC721Transferred(address indexed _token, address indexed _recipient, uint256 _tokenId, bytes _data)`. ```solidity function transferERC721(address _token, address _recipient, uint256 _tokenId, bytes memory _data) external; ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ---------------- | | `_token` | `address` | an ERC721 token | | `_recipient` | `address` | recipient entity | | `_tokenId` | `uint256` | token identifier | | `_data` | `bytes` | byte sequence for `onERC721Received` hook | ### transferEther() Transfers ether to an entity from the contract balance. - reverts if `msg.sender` is not `owner`; - reverts if `_recipient` is zero address; - reverts if the contract balance is insufficient; - reverts if the actual transfer OP fails (e.g. `_recipient` is a contract with no fallback); - emits `EtherTransferred(address indexed _recipient, uint256 _amount)`. ```solidity function transferEther(address _recipient, uint256 _amount) external; ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ---------------- | | `_recipient` | `address` | recipient entity | | `_amount` | `uint256` | transfer amount | ### transferOwnership() Assigns `newOwner` as the `owner`. - reverts if `msg.sender` is not `owner`; - reverts if `newOwner` is zero address; - emits `emit OwnershipTransferred(address indexed previousOwner, address indexed newOwner)`. ```solidity function transferOwnership(address newOwner) public; ``` #### Parameters | Name | Type | Description | | -------- | -------- | -------- | | `newOwner` | `address` | entity which will have access to all state-mutating operations | --- # StakingRouter - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/sr/StakingRouter.sol) - [Deployed contract](https://etherscan.io/address/0xFdDf38947aFB03C621C71b06C9C70bce73f12999) StakingRouter is a registry of staking modules, each encapsulating a certain validator subset, e.g. curated staking module, community staking module. The contract allocates stake to modules, executes validator deposits and top-ups with ether pulled from [Lido](/contracts/lido), distributes protocol fees, and tracks per-module validator balances. ## What is StakingRouter? StakingRouter is a top-level controller contract for staking modules. Each staking module is a contract that, in turn, manages its own subset of validators, e.g. the [Curated](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5) staking module is a set of Lido DAO-vetted node operators. Such modular design opens the opportunity for anyone to build a staking module and join the Lido staking platform, including permissionless community stakers, DVT-enabled validators or any other validator subset, technology or mechanics. StakingRouter performs a number of functions, including: - maintaining a registry of staking modules, - allocating stake to modules, - executing deposits and validator top-ups with ether pulled from [Lido](/contracts/lido), - tracking per-module validator balances and exited validators, and - distributing protocol fees. ## Module Management ### Registering a module Modules are registered with StakingRouter through the Lido DAO voting process. To be considered by the governance, the applying module contract should implement the appropriate module interface, meet security requirements, and have a fee structure aligned with the Lido protocol sustainability. Once voted in, the module starts receiving stake and protocol fees. Staking modules are registered using the `addStakingModule` function, providing a human-readable module name, the address of the deployed staking module contract, and a `StakingModuleConfig` struct with the module parameters: - stake share limit, a relative hard cap on deposits within Lido; - priority exit share threshold, the module's share upon crossing which validator exits from the module are prioritized; - module fee, a percentage of staking rewards to be awarded to the module; - treasury fee, a percentage of staking rewards to be directed to the protocol treasury; - maximum deposits per block and minimum deposit block distance, deposit rate limits; - withdrawal credentials type, either `0x01` or `0x02`. ### Withdrawal credential types Each staking module is permanently configured with a withdrawal credentials type: - `0x01` โ€” modules whose validators have a maximum effective balance of 32 ETH (`MAX_EFFECTIVE_BALANCE_WC_TYPE_01`); - `0x02` โ€” modules whose validators use [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) compounding withdrawal credentials with a maximum effective balance of 2048 ETH (`MAX_EFFECTIVE_BALANCE_WC_TYPE_02`) and support [validator top-ups](#top-ups). The router stores a single protocol-wide withdrawal credentials value that contains the withdrawal address. The effective withdrawal credentials of a particular module are derived by applying the module's type prefix (`0x01` or `0x02`) to that value and can be read via [`getStakingModuleWithdrawalCredentials`](#getstakingmodulewithdrawalcredentials). ### Pausing modules Each staking module has a status: a state that determines whether the module can perform deposits and receive rewards: - `Active`, can make deposits and receives rewards, - `DepositsPaused`, deposits are not allowed but receives rewards, - `Stopped`, cannot make deposits and does not receive rewards. ```solidity enum StakingModuleStatus { Active, // deposits and rewards allowed DepositsPaused, // deposits NOT allowed, rewards allowed Stopped // deposits and rewards NOT allowed } ``` ### Exited validators When the withdrawal requests demand exceed buffered ether sitting in Lido together with projected rewards, the protocol signals to node operators to start exiting validators to cover the withdrawals. In this connection, StakingRouter distinguishes two types of validator states: - [exited](https://hackmd.io/zHYFZr4eRGm3Ju9_vkcSgQ?view) validators, - and late validators, meaning those validators which were requested to exit that failed to exit in a specified timeframe. The StakingRouter tracks exited validators for correct stake allocation and notifies Staking Modules about late validators, enabling penalization actions if needed. ## Stake allocation StakingRouter carries out a vital task of distributing depositable ether to staking modules in a manner that aligns with their growth targets set by the DAO. This design ensures a regulated and controlled growth for the modules that have been newly integrated into the system. The principles governing this methodology are comprehensively discussed in [ADR: Staking Router](https://hackmd.io/f1wvHzpjTIq41-GCrdaMjw?view#Target-shares). ### Deposit The deposit workflow involves submitting batches of 32 ether deposits, along with associated validator keys, to [`DepositContract`](https://ethereum.org/en/staking/deposit-contract/) in one transaction. Given that each staking module handles its own deposits, every batch deposit is restricted to keys originating from a single module. The deposit operation is, at its core, a sequence of contract calls sparked by an off-chain software, the [depositor bot](https://github.com/lidofinance/depositor-bot). This bot gathers guardian messages to confirm that there are no pre-existing keys in the registry that could take advantage of the [frontrunning vulnerability](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-5.md). Once the necessary quorum of guardians is reached, the bot forwards these messages along with the module identifier to the [DepositSecurityModule](/contracts/deposit-security-module) (not to be confused with a staking module). This contract verifies the messages and calls the `deposit` function on StakingRouter. Deposits follow the pull model: buffered ether stays on the [Lido](/contracts/lido) contract until the moment of the deposit, and StakingRouter pulls exactly the amount it is about to deposit. The `deposit` function: 1. verifies that the caller is the DepositSecurityModule and that the staking module is active; 2. determines how much of Lido's depositable ether (`Lido.getDepositableEther()`) can be allocated to the module according to the [allocation algorithm](#allocation-algorithm), and derives the maximum number of deposits as the allocation divided by 32 ether, additionally capped by the module's `maxDepositsPerBlock`; 3. obtains deposit data (public keys and signatures) from the staking module contract via `obtainDepositData`; the module may return fewer keys than requested; 4. records the current timestamp and block number as the module's last deposit state (this happens even if the module returned zero keys); 5. pulls `number of obtained keys ร— 32 ether` from Lido via [`Lido.withdrawDepositableEther()`](/contracts/lido#withdrawdepositableether), which sends the ether back to the router's [`receiveDepositableEther`](#receivedepositableether) within the same call; 6. performs a 32 ether deposit to `DepositContract` for each key, using the module's withdrawal credentials (the module's `0x01` or `0x02` type prefix applied to the protocol-wide withdrawal credentials); 7. confirms that all pulled ether has been deposited by comparing the contract's ether balance before and after the deposits. Direct ether transfers to StakingRouter are rejected: the `receive()` function reverts with the `DirectETHTransfer` error. Ether can enter the contract only through `receiveDepositableEther` as part of a deposit or top-up transaction. ### Top-ups Validators of `0x02` modules can hold an effective balance of up to 2048 ETH. Beyond the initial 32 ether deposit that activates a validator, additional stake reaches these validators via top-ups: the depositor bot calls [`TopUpGateway`](/contracts/top-up-gateway), which verifies each validator's Consensus Layer state with Merkle proofs, computes per-key top-up limits, and calls the router's `topUp` function. The `topUp` function: 1. verifies that the caller is the [TopUpGateway](/contracts/top-up-gateway), that the staking module is active, and that its withdrawal credentials type is `0x02`; 2. determines the module's top-up allocation according to the [allocation algorithm](#allocation-algorithm), capped by the global per-block top-up limit ([`getMaxTopUpPerBlockGwei`](#getmaxtopupperblockgwei)) and rounded down to a whole gwei; 3. calls `allocateDeposits` on the staking module, which validates that the supplied keys belong to the module and returns the exact deposit amount for each key; each amount must be gwei-aligned and within its per-key limit, and the total must not exceed the module allocation; 4. pulls the total amount from Lido via [`Lido.withdrawDepositableEther()`](/contracts/lido#withdrawdepositableether); 5. executes a top-up deposit to `DepositContract` for each key with a non-zero amount; 6. confirms that all pulled ether has been deposited by comparing the contract's ether balance before and after the top-ups. If the module allocation rounds down to zero, the module is still called with a zero amount so that it can advance its internal deposit queue โ€” but only if Lido deposits are enabled; otherwise the call reverts with `LidoDepositsPaused`. ### Allocation algorithm StakingRouter distributes incoming ether across staking modules using the [`MinFirstAllocationStrategy`](https://github.com/lidofinance/core/blob/v4.0.0/contracts/common/lib/MinFirstAllocationStrategy.sol) algorithm: modules with proportionally less stake receive deposits first, gradually equalizing their sizes over time. The strategy operates in 32 ETH units (`MAX_EFFECTIVE_BALANCE_WC_TYPE_01`), so the amount allocated to each module is always a multiple of 32 ETH. The allocation is exposed via the [`getDepositAllocations`](#getdepositallocations) view and is computed as follows: 1. The ether amount to allocate is converted into 32 ETH units (`depositsToAllocate`). 2. The current allocation of each module is calculated in the same units: - for `0x01` modules, it is derived from on-chain accounting as the number of active validators (each holding 32 ETH): `currentAllocation = depositedValidators - max(moduleReportedExited, stakingRouterTrackedExited)`; - for `0x02` modules, the router queries the module directly via `getTotalModuleStake()` to obtain the actual total ether staked in the module and converts it into 32 ETH units, rounded up: `currentAllocation = ceil(getTotalModuleStake() / 32 ETH)`. 3. A `capacities` array is built, where each entry represents the maximum allocation a module can reach. For an active module, the capacity is the minimum of two constraints โ€” the module's stake share limit and its available room: - for initial (seed) deposits: `capacity = min(stakeShareLimit * totalUnits / TOTAL_BASIS_POINTS, currentAllocation + depositableValidatorsCount)`, where `totalUnits` is the sum of all current allocations plus `depositsToAllocate`; - for top-up allocations, a `0x02` module's capacity is measured by the effective balance headroom of its active validators rather than by available keys: `capacity = min(stakeShareLimit * totalUnits / TOTAL_BASIS_POINTS, activeValidatorsCount * maxEBType2 / maxEBType1)`; `0x01` modules keep their seed-deposit capacity to preserve the priority ordering between modules, but the top-up amounts computed for them are never used since `0x01` modules cannot receive top-ups. A module that is not in the `Active` status has its capacity set to the current allocation and receives nothing. 4. `MinFirstAllocationStrategy.allocate` is called with the current allocations, the capacities, and `depositsToAllocate`. It fills the modules with the least allocation first, until either the whole amount is distributed or all modules reach their capacity. The results are converted back to ether amounts. ## Fee distribution The fee structure is set independently in each module. There are two components to the fee structure: the module fee and the treasury fee, both specified as percentages (basis points). For example, a 5% (500 basis points) module fee split between node operators in the module and a 5% (500 basis points) treasury fee sent to the treasury. The sum of the module fee and the treasury fee must be equal across all registered modules; this invariant is enforced on every fee update. Additionally,ย `StakingRouter`ย utilizes a precision factor of 100 \* 10^18ย for fees that prevents arithmetic operations from truncating the fees of small modules. The protocol fee is distributed between modules proportionally to their validator balances and the specified module fee: ``` moduleShare = moduleValidatorsBalance / totalModulesValidatorsBalance ``` where `moduleValidatorsBalance` is the module's CL validators balance (excluding pending deposits) reported by the [AccountingOracle](/contracts/accounting-oracle) and stored on the router (see [Validator balance accounting](#validator-balance-accounting)). For example, a module holding 75% of the total validator balance in the protocol and having a 5% module fee will receive 3.75% of the total rewards across the protocol. A module with a single 2048 ETH validator receives the share of rewards corresponding to its contribution to the total stake, regardless of its validator count. This means that if the modules' fee and treasury fee do not exceed 10%, the total protocol fee will not either, no matter how many modules there are. There is also an edge case where the module is stopped for emergency while its validators are still active. In this case the module fee will be transferred to the treasury and once the module is back online, the rewards will be returned back to the module from the treasury. The distribution function itself works as follows: 1. The function reads the total validator balance across all registered modules. If there are no staking modules or the total balance is zero, it returns an empty response. 2. Otherwise, it initializes arrays to store the module IDs (`stakingModuleIds`), the addresses of reward recipients (`recipients`), and the fees of each recipient (`stakingModuleFees`). It also sets the `precisionPoints` to a constant `FEE_PRECISION_POINTS`, which represents the base precision number that constitutes 100% fee. 3. Then it loops through each staking module, skipping modules with a zero validator balance. For each remaining module, it: - Stores the module ID and recipient address in the respective arrays. - Calculates the module's share as its validator balance divided by the total validator balance across all modules (scaled by `FEE_PRECISION_POINTS`). - Calculates the `stakingModuleFee` as the product of the module's share and the fee of the staking module divided by `TOTAL_BASIS_POINTS`. If the module is not stopped, this fee is stored in the `stakingModuleFees` array. - Adds to `totalFee` the sum of the staking module's fee and a fee going to the treasury (calculated similarly to `stakingModuleFee`), where the treasury is a central pool of funds. 4. After looping through all modules, it makes an assertion that `totalFee` doesn't exceed 100% (represented by `precisionPoints`). 5. If there are staking modules with a zero validator balance, it shrinks the `stakingModuleIds`, `recipients`, and `stakingModuleFees` arrays to exclude those modules. Finally, the function returns five arrays/values: `recipients`, `stakingModuleIds`, `stakingModuleFees`, `totalFee`, and `precisionPoints`. These give the caller an overview of how rewards are distributed amongst the staking modules. ## Validator balance accounting StakingRouter tracks the total balance of active validators for each module (`validatorsBalanceGwei`) and the aggregate balance across all modules. These balances are the basis for [fee distribution](#fee-distribution). The balances are delivered by the [AccountingOracle](/contracts/accounting-oracle) as part of the main report phase via [`reportValidatorBalancesByStakingModule`](#reportvalidatorbalancesbystakingmodule). The report must include all registered staking modules in their registration order, with each value being the sum of the module's validator balances (excluding pending deposits), nominated in gwei. The view counterpart [`validateReportValidatorBalancesByStakingModule`](#validatereportvalidatorbalancesbystakingmodule) allows pre-validating a report against the current module set without mutating state. The stored balances are readable via [`getModuleValidatorsBalance`](#getmodulevalidatorsbalance), [`getTotalModulesValidatorsBalance`](#gettotalmodulesvalidatorsbalance), and [`getStakingModuleStateAccounting`](#getstakingmodulestateaccounting). ## Helpful links - [Staking Router ADR](https://hackmd.io/f1wvHzpjTIq41-GCrdaMjw?view) - [Staking Router LIP](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-20.md) - [LIP-35: Staking Router v3](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md) - Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)) ## View methods ### `getStakingModules` Returns the list of structs of all registered staking modules. Each staking module has an associated data structure, ```solidity struct StakingModule { uint24 id; address stakingModuleAddress; uint16 stakingModuleFee; uint16 treasuryFee; uint16 stakeShareLimit; uint8 status; string name; uint64 lastDepositAt; uint256 lastDepositBlock; uint256 exitedValidatorsCount; uint16 priorityExitShareThreshold; uint64 maxDepositsPerBlock; uint64 minDepositBlockDistance; uint8 withdrawalCredentialsType; uint64 validatorsBalanceGwei; } ``` ```solidity function getStakingModules() external view returns (StakingModule[] memory res); ``` **Returns:** | Name | Type | Description | | ----- | ----------------- | ------------------------------------------------- | | `res` | `StakingModule[]` | list of structs of all registered staking modules | ### `getStakingModuleIds` Returns the list of ids of all registered staking modules. ```solidity function getStakingModuleIds() external view returns (uint256[] memory stakingModuleIds); ``` **Returns:** | Name | Type | Description | | ------------------ | ----------- | --------------------------------- | | `stakingModuleIds` | `uint256[]` | list of id of all staking modules | ### `getStakingModule` Returns the struct of the specified staking module by its id. ```solidity function getStakingModule(uint256 _stakingModuleId) external view returns (StakingModule memory); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------------- | -------------------------- | | | `StakingModule` | staking module information | ### `getStakingModulesCount` Returns the number of registered staking modules. ```solidity function getStakingModulesCount() external view returns (uint256); ``` ### `hasStakingModule` Return a boolean value indicating whether a staking module with the specified id is registered. ```solidity function hasStakingModule(uint256 _stakingModuleId) public view returns (bool); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | ### `getStakingModuleStatus` Return the status of the staking module. ```solidity function getStakingModuleStatus(uint256 _stakingModuleId) public view returns (StakingModuleStatus); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------------------- | ---------------------------- | | | `StakingModuleStatus` | status of the staking module | ### `getStakingModuleStateConfig` Returns the configuration part of the staking module state: the module address, fees, share limits, status, and withdrawal credentials type. ```solidity struct ModuleStateConfig { address moduleAddress; uint16 moduleFee; uint16 treasuryFee; uint16 stakeShareLimit; uint16 priorityExitShareThreshold; StakingModuleStatus status; uint8 withdrawalCredentialsType; } ``` ```solidity function getStakingModuleStateConfig(uint256 _stakingModuleId) external view returns (ModuleStateConfig memory stateConfig); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ------------- | ------------------- | ------------------------------------------- | | `stateConfig` | `ModuleStateConfig` | configuration part of the module state | ### `getStakingModuleStateDeposits` Returns the deposit-related part of the staking module state: the last deposit timestamp and block, the maximum number of deposits per block, and the minimum deposit block distance. ```solidity struct ModuleStateDeposits { uint64 lastDepositAt; uint64 lastDepositBlock; uint64 maxDepositsPerBlock; uint64 minDepositBlockDistance; } ``` ```solidity function getStakingModuleStateDeposits(uint256 _stakingModuleId) external view returns (ModuleStateDeposits memory stateDeposits); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | --------------- | --------------------- | ---------------------------------------- | | `stateDeposits` | `ModuleStateDeposits` | deposit-related part of the module state | ### `getStakingModuleStateAccounting` Returns the accounting part of the staking module state: the total balance of the module's active validators (in gwei) and the total exited validators count tracked by StakingRouter for the module. ```solidity function getStakingModuleStateAccounting(uint256 _stakingModuleId) external view returns (uint64 validatorsBalanceGwei, uint64 exitedValidatorsCount); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ----------------------- | -------- | --------------------------------------------------------------- | | `validatorsBalanceGwei` | `uint64` | total balance of the module's active validators, in gwei | | `exitedValidatorsCount` | `uint64` | total exited validators count tracked by the router for the module | ### `getStakingModuleSummary` Returns the struct containing a short summary of validators in the specified staking module, as shown below, ```solidity struct StakingModuleSummary { uint256 totalExitedValidators; uint256 totalDepositedValidators; uint256 depositableValidatorsCount; } ``` ```solidity function getStakingModuleSummary(uint256 _stakingModuleId) external view returns (StakingModuleSummary memory summary); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | ---------------------- | ------------------------------------------ | | | `StakingModuleSummary` | summary of the staking module's validators | ### `getNodeOperatorSummary` Returns the summary of a node operator from the staking module, as shown below, ```solidity struct NodeOperatorSummary { uint256 targetLimitMode; uint256 targetValidatorsCount; uint256 stuckValidatorsCount; // DEPRECATED: always zero, kept for backward compatibility uint256 refundedValidatorsCount; // DEPRECATED: always zero, kept for backward compatibility uint256 stuckPenaltyEndTimestamp; // DEPRECATED: always zero, kept for backward compatibility uint256 totalExitedValidators; uint256 totalDepositedValidators; uint256 depositableValidatorsCount; } ``` ```solidity function getNodeOperatorSummary( uint256 _stakingModuleId, uint256 _nodeOperatorId ) external view returns (NodeOperatorSummary memory summary); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | | `_nodeOperatorId` | `uint256` | node operator id | **Returns:** | Name | Type | Description | | ---- | --------------------- | ---------------------------- | | | `NodeOperatorSummary` | summary of the node operator | ### `getAllStakingModuleDigests` Returns the digests of all staking modules, as show below, ```solidity struct StakingModuleDigest { uint256 nodeOperatorsCount; uint256 activeNodeOperatorsCount; StakingModule state; StakingModuleSummary summary; } ``` ```solidity function getAllStakingModuleDigests() external view returns (StakingModuleDigest[]); ``` **Returns:** | Name | Type | Description | | ---- | ----------------------- | ------------------------------- | | | `StakingModuleDigest[]` | array of staking module digests | ### `getStakingModuleDigests` Returns the digest of the specified staking modules. ```solidity function getStakingModuleDigests(uint256[] memory _stakingModuleIds) public view returns (StakingModuleDigest[]); ``` **Parameters:** | Name | Type | Description | | ------------------- | ----------- | --------------------------- | | `_stakingModuleIds` | `uint256[]` | array of staking module ids | **Returns:** | Name | Type | Description | | ---- | ----------------------- | ------------------------------- | | | `StakingModuleDigest[]` | array of staking module digests | ### `getAllNodeOperatorDigests` Returns the digests of all node operators in the specified staking module, ```solidity struct NodeOperatorDigest { uint256 id; bool isActive; NodeOperatorSummary summary; } ``` ```solidity function getAllNodeOperatorDigests(uint256 _stakingModuleId) external view returns (NodeOperatorDigest[]); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | ---------------------- | ------------------------------ | | | `NodeOperatorDigest[]` | array of node operator digests | ### `getNodeOperatorDigests` Returns the digests for the specified node operators in the staking module. ```solidity function getNodeOperatorDigests(uint256 _stakingModuleId, uint256[] memory _nodeOperatorIds) public view returns (NodeOperatorDigest[]); ``` **Parameters:** | Name | Type | Description | | ------------------ | ----------- | -------------------------- | | `_stakingModuleId` | `uint256` | staking module id | | `_nodeOperatorIds` | `uint256[]` | array of node operator ids | **Returns:** | Name | Type | Description | | ---- | ---------------------- | ------------------------------ | | | `NodeOperatorDigest[]` | array of node operator digests | ### `getStakingModuleIsStopped` Return a boolean value whether the staking module is stopped. ```solidity function getStakingModuleIsStopped(uint256 _stakingModuleId) external view returns (bool); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | ------ | ------------------------------------------------------ | | | `bool` | true if the staking module is stopped, false otherwise | ### `getStakingModuleIsDepositsPaused` Return a boolean value whether deposits are paused for the staking module. ```solidity function getStakingModuleIsDepositsPaused(uint256 _stakingModuleId) external view returns (bool); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | ------ | ------------------------------------------------------------------- | | | `bool` | true if deposits are paused for the staking module, false otherwise | ### `getStakingModuleIsActive` Return a boolean value whether the staking module is active. ```solidity function getStakingModuleIsActive(uint256 _stakingModuleId) external view returns (bool); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | ------ | ----------------------------------------------------- | | | `bool` | true if the staking module is active, false otherwise | ### `getStakingModuleNonce` Get the nonce of a staking module. ```solidity function getStakingModuleNonce(uint256 _stakingModuleId) external view returns (uint256); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | --------------------------- | | | `uint256` | nonce of the staking module | ### `getStakingModuleLastDepositBlock` Get the block number of the last deposit to the staking module. ```solidity function getStakingModuleLastDepositBlock(uint256 _stakingModuleId) external view returns (uint256); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | -------------------------------- | | | `uint256` | block number of the last deposit | ### `getStakingModuleMinDepositBlockDistance` Get the min deposit block distance for the staking module ```solidity function getStakingModuleMinDepositBlockDistance(uint256 _stakingModuleId) external view returns (uint256); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | ------------------------------------------------- | | | `uint256` | min deposit block distance for the staking module | ### `getStakingModuleMaxDepositsPerBlock` Get the max deposits count per block for the staking module ```solidity function getStakingModuleMaxDepositsPerBlock(uint256 _stakingModuleId) external view returns (uint256); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | --------------------------------------------------- | | | `uint256` | Max deposits count per block for the staking module | ### `getStakingModuleActiveValidatorsCount` Returns the number of active validators in the staking module. ```solidity function getStakingModuleActiveValidatorsCount(uint256 _stakingModuleId) external view returns (uint256 activeValidatorsCount); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | --------------------------- | | | `uint256` | number of active validators | ### `getModuleValidatorsBalance` Returns the total balance of the staking module's active validators used for [fee distribution](#fee-distribution), in wei. See [Validator balance accounting](#validator-balance-accounting). ```solidity function getModuleValidatorsBalance(uint256 moduleId) external view returns (uint256); ``` **Parameters:** | Name | Type | Description | | ---------- | --------- | ----------------- | | `moduleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | -------------------------------------------------------- | | | `uint256` | total balance of the module's active validators, in wei | ### `getTotalModulesValidatorsBalance` Returns the sum of active validators balances across all registered staking modules, in wei. ```solidity function getTotalModulesValidatorsBalance() external view returns (uint256); ``` **Returns:** | Name | Type | Description | | ---- | --------- | ------------------------------------------------------- | | | `uint256` | total balance of validators across all modules, in wei | ### `getStakingModuleMaxDepositsCount` Calculates the maximum number of deposits a staking module can handle based on the available deposit value: the module's [allocation](#allocation-algorithm) for that amount divided by 32 ether. ```solidity function getStakingModuleMaxDepositsCount( uint256 _stakingModuleId, uint256 _maxDepositsValue ) public view returns (uint256); ``` **Parameters:** | Name | Type | Description | | ------------------- | --------- | ------------------------------------------------------- | | `_stakingModuleId` | `uint256` | staking module id | | `_maxDepositsValue` | `uint256` | maximum amount of deposits based on the available ether | **Returns:** | Name | Type | Description | | ---- | --------- | -------------------------------------------------------------------------- | | | `uint256` | maximum number of deposits that can be made using the given staking module | ### `getStakingFeeAggregateDistribution` Returns the total fee distribution proportion. ```solidity function getStakingFeeAggregateDistribution() public view returns ( uint96 modulesFee, uint96 treasuryFee, uint256 basePrecision ); ``` **Returns:** | Name | Type | Description | | --------------- | --------- | ------------------------------------------------------------ | | `modulesFee` | `uint96` | total fees for all staking modules | | `treasuryFee` | `uint96` | total fee for the treasury | | `basePrecision` | `uint256` | base precision number, a value corresponding to the full fee | ### `getStakingRewardsDistribution` Get the shares table. ```solidity function getStakingRewardsDistribution() public view returns ( address[] memory recipients, uint256[] memory stakingModuleIds, uint96[] memory stakingModuleFees, uint96 totalFee, uint256 precisionPoints ); ``` **Returns:** | Name | Type | Description | | ------------------- | ----------- | ------------------------------------------------------------ | | `recipients` | `address[]` | total staking module addresses | | `stakingModuleIds` | `uint256[]` | staking module ids | | `stakingModuleFees` | `uint96[]` | staking module fees | | `totalFee` | `uint96` | total fee | | `precisionPoints` | `uint256` | base precision number, a value corresponding to the full fee | ### `getDepositAllocations` Calculates the ether allocation between staking modules after the distribution of the `_depositAmount` amount using the [allocation algorithm](#allocation-algorithm). With `_isTopUp` set to `true`, calculates the [top-up](#top-ups) allocation; with `false`, the initial-deposit allocation. ```solidity function getDepositAllocations(uint256 _depositAmount, bool _isTopUp) public view returns ( uint256 totalAllocated, uint256[] memory allocated, uint256[] memory newAllocations ); ``` **Parameters:** | Name | Type | Description | | ---------------- | --------- | ----------------------------------------------------------------------------- | | `_depositAmount` | `uint256` | maximum ether amount of deposits to be allocated between staking modules | | `_isTopUp` | `bool` | whether the allocation is requested for top-ups (true) or initial deposits (false) | **Returns:** | Name | Type | Description | | ---------------- | ----------- | ---------------------------------------------------- | | `totalAllocated` | `uint256` | ether amount actually allocated | | `allocated` | `uint256[]` | array of newly allocated ether amounts per module | | `newAllocations` | `uint256[]` | array of resulting allocation amounts per module | ### `getWithdrawalCredentials` Get the protocol-wide withdrawal credentials containing the withdrawal address. The per-module credentials are derived from this value by applying the module's type prefix; see [`getStakingModuleWithdrawalCredentials`](#getstakingmodulewithdrawalcredentials). ```solidity function getWithdrawalCredentials() public view returns (bytes32); ``` **Returns:** | Name | Type | Description | | ---- | --------- | ---------------------- | | | `bytes32` | withdrawal credentials | ### `getStakingModuleWithdrawalCredentials` Get the withdrawal credentials of the staking module: the protocol-wide withdrawal credentials with the module's type prefix applied โ€” `0x01...` for `0x01` modules, `0x02...` for `0x02` modules. ```solidity function getStakingModuleWithdrawalCredentials(uint256 _stakingModuleId) external view returns (bytes32); ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ----------------- | | `_stakingModuleId` | `uint256` | staking module id | **Returns:** | Name | Type | Description | | ---- | --------- | -------------------------------------------- | | | `bytes32` | withdrawal credentials of the staking module | ### `getMaxTopUpPerBlockGwei` Returns the global per-block limit for [validator top-ups](#top-ups), in gwei. ```solidity function getMaxTopUpPerBlockGwei() external view returns (uint64); ``` **Returns:** | Name | Type | Description | | ---- | -------- | ---------------------------------- | | | `uint64` | per-block top-up limit, in gwei | ### `validateReportValidatorBalancesByStakingModule` Validates a validator balances report against the current staking module set: the arrays must cover all registered staking modules in their registration order, and each balance value must not exceed the sanity limit. The view counterpart of [`reportValidatorBalancesByStakingModule`](#reportvalidatorbalancesbystakingmodule) used to pre-validate report data without mutating state. ```solidity function validateReportValidatorBalancesByStakingModule( uint256[] calldata _stakingModuleIds, uint256[] calldata _validatorBalancesGwei ) external view; ``` **Parameters:** | Name | Type | Description | | ------------------------ | ----------- | ------------------------------------------------------------------------------ | | `_stakingModuleIds` | `uint256[]` | ids of all registered staking modules in their registration order | | `_validatorBalancesGwei` | `uint256[]` | validator balances for the specified staking modules, in gwei | ### `getContractVersion` Returns the current initialized version of the contract. ```solidity function getContractVersion() external view returns (uint256); ``` **Returns:** | Name | Type | Description | | ---- | --------- | ------------------------------------------- | | | `uint256` | current initialized version of the contract | ## Write methods ### `deposit` Invokes a batch of 32 ether deposit calls to the official [`DepositContract`](https://ethereum.org/en/staking/deposit-contract/) using the keys of the specified staking module. The router calculates the module [allocation](#allocation-algorithm), obtains deposit data from the module, pulls the required ether from [Lido](/contracts/lido) via [`Lido.withdrawDepositableEther()`](/contracts/lido#withdrawdepositableether), and performs a 32 ether deposit for each key using the module's withdrawal credentials. See [Deposit](#deposit) for the detailed flow. Can be called only by the [DepositSecurityModule](/contracts/deposit-security-module) contract. ```solidity function deposit(uint256 _stakingModuleId, bytes calldata _depositCalldata) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | -------------------------------------- | | `_stakingModuleId` | `uint256` | id of the staking module to deposit to | | `_depositCalldata` | `bytes` | staking module calldata | ### `topUp` Performs top-up deposits to the official [`DepositContract`](https://ethereum.org/en/staking/deposit-contract/) for existing validators of a `0x02` staking module. The router determines how much ether can be deposited to the module, obtains the exact per-key amounts from the module via `allocateDeposits`, pulls the total ether from [Lido](/contracts/lido), and executes the top-ups. See [Top-ups](#top-ups) for the detailed flow. Can be called only by the [TopUpGateway](/contracts/top-up-gateway) contract. ```solidity function topUp( uint256 _stakingModuleId, uint256[] calldata _keyIndices, uint256[] calldata _operatorIds, bytes[] calldata _pubkeys, uint256[] calldata _topUpLimits ) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | ----------- | ---------------------------------------------------------------------------------------- | | `_stakingModuleId` | `uint256` | id of the staking module to deposit to | | `_keyIndices` | `uint256[]` | list of keys' indices | | `_operatorIds` | `uint256[]` | list of node operator ids | | `_pubkeys` | `bytes[]` | list of validator public keys to top up | | `_topUpLimits` | `uint256[]` | maximum amount (in wei) that can be deposited per key based on CL data and TopUpGateway logic | ### `receiveDepositableEther` A payable function for depositable ether acquisition: the single entry point for ether into the contract, invoked within [`Lido.withdrawDepositableEther()`](/contracts/lido#withdrawdepositableether) during [deposits](#deposit) and [top-ups](#top-ups). Any other direct ether transfer to StakingRouter reverts. Can be called only by the [Lido](/contracts/lido) contract. ```solidity function receiveDepositableEther() external payable; ``` ### `addStakingModule` Register a staking module. Restricted to the `STAKING_MODULE_MANAGE_ROLE` role. ```solidity struct StakingModuleConfig { uint256 stakeShareLimit; uint256 priorityExitShareThreshold; uint256 stakingModuleFee; uint256 treasuryFee; uint256 maxDepositsPerBlock; uint256 minDepositBlockDistance; uint256 withdrawalCredentialsType; } ``` ```solidity function addStakingModule( string calldata _name, address _stakingModuleAddress, StakingModuleConfig calldata _stakingModuleConfig ) external; ``` **Parameters:** | Name | Type | Description | | ----------------------- | --------------------- | -------------------------------------------------------------------- | | `_name` | `string` | human-readable name of the module | | `_stakingModuleAddress` | `address` | address of the module contract | | `_stakingModuleConfig` | `StakingModuleConfig` | staking module configuration | `StakingModuleConfig` fields: | Name | Type | Description | | ---------------------------- | --------- | -------------------------------------------------------------------- | | `stakeShareLimit` | `uint256` | maximum share that can be allocated to a module, in basis points | | `priorityExitShareThreshold` | `uint256` | module's priority exit share threshold, in basis points | | `stakingModuleFee` | `uint256` | fee of the staking module taken from the staking rewards, in basis points | | `treasuryFee` | `uint256` | treasury fee, in basis points | | `maxDepositsPerBlock` | `uint256` | maximum number of validators that can be deposited in a single block | | `minDepositBlockDistance` | `uint256` | minimum distance between deposits in blocks | | `withdrawalCredentialsType` | `uint256` | withdrawal credentials type of the module (`0x01` or `0x02`) | ### `updateStakingModule` Update the parameters of a staking module. The withdrawal credentials type cannot be changed. Restricted to the `STAKING_MODULE_MANAGE_ROLE` role. ```solidity function updateStakingModule( uint256 _stakingModuleId, uint256 _stakeShareLimit, uint256 _priorityExitShareThreshold, uint256 _stakingModuleFee, uint256 _treasuryFee, uint256 _maxDepositsPerBlock, uint256 _minDepositBlockDistance ) external; ``` **Parameters:** | Name | Type | Description | | ----------------------------- | --------- | -------------------------------------------------------------------- | | `_stakingModuleId` | `uint256` | id of the module | | `_stakeShareLimit` | `uint256` | maximum share that can be allocated to a module | | `_priorityExitShareThreshold` | `uint256` | Module's priority exit share threshold | | `_stakingModuleFee` | `uint256` | updated module fee | | `_treasuryFee` | `uint256` | updated module treasury fee | | `_maxDepositsPerBlock` | `uint256` | maximum number of validators that can be deposited in a single block | | `_minDepositBlockDistance` | `uint256` | minimum distance between deposits in blocks | ### `updateAllStakingModulesFees` Updates fees for all staking modules in a single atomic operation. The values must be provided in the module registration order (as returned by `getStakingModuleIds()`), and the sum `staking module fee + treasury fee` must be equal across all modules. Restricted to the `STAKING_MODULE_MANAGE_ROLE` role. ```solidity function updateAllStakingModulesFees( uint256[] calldata _stakingModuleFees, uint256[] calldata _treasuryFees ) external; ``` **Parameters:** | Name | Type | Description | | -------------------- | ----------- | ----------------------------------------------------------------- | | `_stakingModuleFees` | `uint256[]` | new staking module fee values in the module registration order | | `_treasuryFees` | `uint256[]` | new treasury fee values in the module registration order | ### `updateModuleShares` Updates the share-related parameters of a staking module. Restricted to the `STAKING_MODULE_SHARE_MANAGE_ROLE` role, which allows for granular permissions on share management separate from the full module management. ```solidity function updateModuleShares( uint256 _stakingModuleId, uint16 _stakeShareLimit, uint16 _priorityExitShareThreshold ) external; ``` **Parameters:** | Name | Type | Description | | ----------------------------- | --------- | ------------------------------------------------ | | `_stakingModuleId` | `uint256` | id of the module | | `_stakeShareLimit` | `uint16` | new stake share limit value, in basis points | | `_priorityExitShareThreshold` | `uint16` | new priority exit share threshold, in basis points | ### `setMaxTopUpPerBlockGwei` Sets the global per-block limit for [validator top-ups](#top-ups), in gwei. The value must be greater than zero and fit into `uint64`. Restricted to the `STAKING_MODULE_MANAGE_ROLE` role. ```solidity function setMaxTopUpPerBlockGwei(uint256 _newValue) external; ``` **Parameters:** | Name | Type | Description | | ----------- | --------- | ---------------------------------- | | `_newValue` | `uint256` | new per-block top-up limit, in gwei | ### `updateTargetValidatorsLimits` Updates the limit of the validators that can be used for deposit. Restricted to the `STAKING_MODULE_MANAGE_ROLE` role. ```solidity function updateTargetValidatorsLimits( uint256 _stakingModuleId, uint256 _nodeOperatorId, uint256 _targetLimitMode, uint256 _targetLimit ) external; ``` **Parameters:** | Name | Type | Description | | ------------------ | --------- | ------------------------------------------------------- | | `_stakingModuleId` | `uint256` | id of the module | | `_nodeOperatorId` | `uint256` | id of the node operator | | `_targetLimitMode` | `uint256` | target limit mode (0 = disabled, 1 = soft, 2 = boosted) | | `_targetLimit` | `uint256` | target limit validators count of the node operator | ### `reportRewardsMinted` Reports the minted rewards to the staking modules with the specified ids. Restricted to the `REPORT_REWARDS_MINTED_ROLE` role. ```solidity function reportRewardsMinted( uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares ) external; ``` **Parameters:** | Name | Type | Description | | ------------------- | ----------- | ------------------------------------------------ | | `_stakingModuleIds` | `uint256[]` | list of the reported staking module ids | | `_totalShares` | `uint256[]` | total shares minted to the given staking modules | ### `updateExitedValidatorsCountByStakingModule` Update total numbers of exited validators for staking modules with the specified module ids. Called by the [AccountingOracle](/contracts/accounting-oracle); restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. ```solidity function updateExitedValidatorsCountByStakingModule( uint256[] calldata _stakingModuleIds, uint256[] calldata _exitedValidatorsCounts ) external returns (uint256); ``` **Parameters:** | Name | Type | Description | | ------------------------- | ----------- | ----------------------------------------------------------------- | | `_stakingModuleIds` | `uint256[]` | list of the reported staking module ids | | `_exitedValidatorsCounts` | `uint256[]` | new counts of exited validators for the specified staking modules | **Returns:** | Name | Type | Description | | ---- | --------- | ---------------------------------------------------------------------------------- | | | `uint256` | total increase in the aggregate number of exited validators across the updated modules | ### `reportValidatorBalancesByStakingModule` Stores per-module validator balances used for [fee distribution](#fee-distribution); see [Validator balance accounting](#validator-balance-accounting). The report must include all registered staking modules in their registration order; each value is the sum of the module's validator balances (excluding pending deposits), nominated in gwei. Called by the [AccountingOracle](/contracts/accounting-oracle) as part of the main report phase; restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. ```solidity function reportValidatorBalancesByStakingModule( uint256[] calldata _stakingModuleIds, uint256[] calldata _validatorBalancesGwei ) external; ``` **Parameters:** | Name | Type | Description | | ------------------------ | ----------- | ------------------------------------------------------------------ | | `_stakingModuleIds` | `uint256[]` | ids of all registered staking modules in their registration order | | `_validatorBalancesGwei` | `uint256[]` | validator balances for the specified staking modules, in gwei | ### `reportStakingModuleExitedValidatorsCountByNodeOperator` Updates exited validators counts per node operator for the staking module with the specified id. Restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. ```solidity function reportStakingModuleExitedValidatorsCountByNodeOperator( uint256 _stakingModuleId, bytes calldata _nodeOperatorIds, bytes calldata _exitedValidatorsCounts ) external; ``` **Parameters:** | Name | Type | Description | | ------------------------- | --------- | ---------------------------------------------------------------- | | `_stakingModuleId` | `uint256` | staking module id | | `_nodeOperatorIds` | `bytes` | ids of the node operators | | `_exitedValidatorsCounts` | `bytes` | new counts of exited validators for the specified node operators | ### `unsafeSetExitedValidatorsCount` DEPRECATED. Sets exited validators count for the given module and given node operator in that module without performing critical safety checks, e.g. that exited validators count cannot decrease. Should only be used by the DAO in extreme cases and with sufficient precautions to correct invalid data reported by the oracle committee due to a bug in the oracle daemon. Restricted to the `UNSAFE_SET_EXITED_VALIDATORS_ROLE` role. ```solidity function unsafeSetExitedValidatorsCount( uint256 _stakingModuleId, uint256 _nodeOperatorId, bool _triggerUpdateFinish, ValidatorsCountsCorrection calldata _correction ) external; ``` where `ValidatorsCountsCorrection` is a struct as seen below, ```solidity struct ValidatorsCountsCorrection { /// @notice The expected current number of exited validators of the module that is /// being corrected. uint256 currentModuleExitedValidatorsCount; /// @notice The expected current number of exited validators of the node operator /// that is being corrected. uint256 currentNodeOperatorExitedValidatorsCount; /// @notice The corrected number of exited validators of the module. uint256 newModuleExitedValidatorsCount; /// @notice The corrected number of exited validators of the node operator. uint256 newNodeOperatorExitedValidatorsCount; } ``` **Parameters:** | Name | Type | Description | | ---------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------- | | `_stakingModuleId` | `uint256` | staking module id | | `_nodeOperatorId` | `uint256` | id of the node operator | | `_triggerUpdateFinish` | `bool` | flag to call `onExitedAndStuckValidatorsCountsUpdated` on the module after applying the corrections | | `_correction` | `ValidatorsCountsCorrection` | correction details | ### `onValidatorsCountsByNodeOperatorReportingFinished` Post-report hook called by the oracle when the second phase of data reporting finishes, i.e. when the oracle submitted the complete data on the exited validator counts per node operator for the current reporting frame. Restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. ```solidity function onValidatorsCountsByNodeOperatorReportingFinished() external; ``` ### `decreaseStakingModuleVettedKeysCountByNodeOperator` Decreases vetted signing keys counts per node operator for the staking module with the specified id. Method is called by DSM during the unvetting process. Restricted to the `STAKING_MODULE_UNVETTING_ROLE` role. ```solidity function decreaseStakingModuleVettedKeysCountByNodeOperator( uint256 _stakingModuleId, bytes calldata _nodeOperatorIds, bytes calldata _vettedSigningKeysCounts ) external; ``` **Parameters:** | Name | Type | Description | | -------------------------- | --------- | ------------------------------------------------------------------- | | `_stakingModuleId` | `uint256` | staking module id | | `_nodeOperatorIds` | `bytes` | ids of the node operators | | `_vettedSigningKeysCounts` | `bytes` | new counts of vetted signing keys for the specified node operators. | ### `reportValidatorExitDelay` Reports a validator that was requested to exit, but has not exited yet. The report is used to track potential exitโ€‘delay penalties for the responsible node operator within the specified staking module. Called by the designated verifier contract ([ValidatorExitDelayVerifier](/contracts/validator-exit-delay-verifier)) that verifies validator status on Consensus Layer. Restricted to the `REPORT_VALIDATOR_EXITING_STATUS_ROLE` role. ```solidity function reportValidatorExitDelay( uint256 _stakingModuleId, uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, uint256 _eligibleToExitInSec ) external; ``` Parameters: | Name | Type | Description | | ---------------------- | --------- | ------------------------------------------------------------------------ | | `_stakingModuleId` | `uint256` | Staking module id | | `_nodeOperatorId` | `uint256` | Node operator id within the specified staking module | | `_proofSlotTimestamp` | `uint256` | Beacon slot timestamp used as a proof reference for the validator status | | `_publicKey` | `bytes` | Validator BLS public key | | `_eligibleToExitInSec` | `uint256` | How many seconds the validator has been eligible to exit up to the proof | ### `onValidatorExitTriggered` Handles triggerable exit events for a set of validators, notifying the corresponding staking modules. Called by the [Triggerable Withdrawals Gateway](/contracts/triggerable-withdrawals-gateway). Restricted to the `REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE` role. ```solidity struct ValidatorExitData { uint256 stakingModuleId; uint256 nodeOperatorId; bytes pubkey; } ``` ```solidity function onValidatorExitTriggered( ValidatorExitData[] calldata validatorExitData, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external; ``` Parameters: | Name | Type | Description | | --------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------- | | `validatorExitData` | `ValidatorExitData[]` | array of validators for which a triggerable exit was requested (staking module id, node operator id, public key) | | `_withdrawalRequestPaidFee` | `uint256` | Fee paid to submit the withdrawal request on the Execution Layer | | `_exitType` | `uint256` | Exit trigger type code; may be interpreted differently across staking modules | --- # StakingVault - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/StakingVault.sol) - [Implementation](https://etherscan.io/address/0x06A56487494aa080deC7Bf69128EdA9225784553) - [Beacon](https://etherscan.io/address/0x5FbE8cEf9CCc56ad245736D3C5bAf82ad54Ca789) Isolated staking position with 0x02 withdrawal credentials. Holds validator funds and supports minting stETH through VaultHub while preserving non-custodial ownership. Individual vaults are deployed as beacon proxies by [VaultFactory](/contracts/staking-vault-factory). ## What is StakingVault? A StakingVault is the core stVault primitive: - holds ETH and validator balances tied to its withdrawal credentials - exposes owner and node operator controls - supports PDG-driven and direct deposits - integrates with VaultHub for minting, burn, and health constraints Vaults are deployed via `VaultFactory` as beacon proxies. The deployed contract above is the implementation used by the beacon. ## How it works - The **vault owner** controls funding, withdrawals, exit requests, and administrative functions. - The **node operator** can force-eject validators via EIP-7002 (does not require owner cooperation). - The **depositor** role performs beacon chain deposits and staging operations. - When connected to VaultHub, deposits and withdrawals are gated by collateral rules. The vault is a **PinnedBeaconProxy** instance, meaning it can be ossified (pinned) to prevent future upgrades once disconnected from VaultHub. ## Constants | Constant | Value | Description | | ------------------- | ------------- | --------------------------------------- | | `_VERSION` | 1 | Contract version on implementation | | `WC_0X02_PREFIX` | `0x02 << 248` | Withdrawal credentials type prefix | | `PUBLIC_KEY_LENGTH` | 48 | Length of validator public key in bytes | ## Immutable variables | Variable | Description | | ------------------ | ----------------------------------------- | | `DEPOSIT_CONTRACT` | Address of the BeaconChainDepositContract | ## Storage ```solidity struct Storage { address nodeOperator; // Node operator address (slot 1) address depositor; // Depositor address (slot 2) bool beaconChainDepositsPaused; // Whether deposits are paused (slot 2) uint256 stagedBalance; // ETH staged for activation (slot 3) } ``` ## Staged balance Staged balance is ETH reserved for validator activations. This mechanism supports the Predeposit Guarantee (PDG) flow: 1. **Staging**: When preparing validators via PDG, the depositor calls `stage()` to reserve 31 ETH per validator (the remaining 1 ETH comes from the predeposit). 2. **Staged funds are locked**: Staged ETH cannot be withdrawn via `withdraw()` - only `availableBalance()` (unstaged funds) can be withdrawn. 3. **Activation**: When a validator is activated via `depositFromStaged()`, the staged ETH is consumed for the beacon chain deposit. 4. **Unstaging**: If a predeposit fails or is cancelled, `unstage()` releases the ETH back to available balance. The invariant is: every 1 ETH predeposit in PDG must be paired with 31 ETH staged in the vault for the full 32 ETH validator activation. ``` Total vault balance = availableBalance() + stagedBalance() availableBalance() = address(this).balance - stagedBalance ``` ## Structs ### Deposit Validator deposit data for beacon chain deposits: ```solidity struct Deposit { bytes pubkey; // Validator public key (48 bytes) bytes signature; // BLS signature (96 bytes) uint256 amount; // Deposit amount in wei bytes32 depositDataRoot; // Deposit data root for verification } ``` ## View methods ### getInitializedVersion() ```solidity function getInitializedVersion() external view returns (uint64) ``` Returns the highest version that has been initialized. ### version() ```solidity function version() external pure returns (uint64) ``` Returns contract version constant (1). ### owner() ```solidity function owner() public view returns (address) ``` Returns vault owner. ### pendingOwner() ```solidity function pendingOwner() public view returns (address) ``` Returns pending owner for 2-step ownership transfer. ### nodeOperator() ```solidity function nodeOperator() public view returns (address) ``` Returns node operator address. ### depositor() ```solidity function depositor() public view returns (address) ``` Returns depositor address. ### withdrawalCredentials() ```solidity function withdrawalCredentials() public view returns (bytes32) ``` Returns vault withdrawal credentials (0x02 type). Computed as `0x02 << 248 | address(this)`. ### calculateValidatorWithdrawalFee(uint256 \_numberOfKeys) ```solidity function calculateValidatorWithdrawalFee(uint256 _numberOfKeys) external view returns (uint256) ``` Returns fee for validator withdrawals via EIP-7002. Note: fee may change block to block. ### availableBalance() ```solidity function availableBalance() public view returns (uint256) ``` Returns ETH currently available for withdrawal (total balance minus staged balance). ### stagedBalance() ```solidity function stagedBalance() external view returns (uint256) ``` Returns ETH staged for validator activation. ### beaconChainDepositsPaused() ```solidity function beaconChainDepositsPaused() external view returns (bool) ``` Returns whether beacon chain deposits are paused. ## Methods ### initialize(address \_owner, address \_nodeOperator, address \_depositor) ```solidity function initialize(address _owner, address _nodeOperator, address _depositor) external initializer ``` Initializes the vault with owner, node operator, and depositor roles. ### fund() ```solidity function fund() external payable onlyOwner ``` Adds ETH to the vault. Reverts if `msg.value` is zero. ### withdraw(address \_recipient, uint256 \_ether) ```solidity function withdraw(address _recipient, uint256 _ether) external onlyOwner ``` Withdraws ETH to a recipient. Only withdraws from `availableBalance()` (staged funds are protected). ### pauseBeaconChainDeposits() ```solidity function pauseBeaconChainDeposits() external onlyOwner ``` Pauses beacon chain deposits. Reverts if already paused. ### resumeBeaconChainDeposits() ```solidity function resumeBeaconChainDeposits() external onlyOwner ``` Resumes beacon chain deposits. Reverts if already resumed. ### depositToBeaconChain(Deposit \_deposit) ```solidity function depositToBeaconChain(Deposit calldata _deposit) external onlyDepositor whenDepositsNotPaused ``` Deposits validator data directly to beacon chain from available balance. ### stage(uint256 \_ether) ```solidity function stage(uint256 _ether) external onlyDepositor whenDepositsNotPaused ``` Stages ETH for validator activation. Moves funds from available to staged balance. ### unstage(uint256 \_ether) ```solidity function unstage(uint256 _ether) public onlyDepositor ``` Unstages ETH to make it withdrawable again. Not affected by deposit pause. ### depositFromStaged(Deposit \_deposit, uint256 \_additionalAmount) ```solidity function depositFromStaged(Deposit calldata _deposit, uint256 _additionalAmount) external onlyDepositor ``` Deposits using staged balance plus optional top-up from available balance. **Note:** If `_additionalAmount` is zero, this operation is **not** affected by deposit pause - only the staged portion is used. ### requestValidatorExit(bytes \_pubkeys) ```solidity function requestValidatorExit(bytes calldata _pubkeys) external onlyOwner ``` Requests validator exits by emitting events. Does not directly trigger exits - node operators must monitor for `ValidatorExitRequested` events. ### triggerValidatorWithdrawals(...) ```solidity function triggerValidatorWithdrawals( bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _excessRefundRecipient ) external payable onlyOwner ``` Triggers validator withdrawals via EIP-7002. Requires `msg.value` to cover fees. - If `_amountsInGwei` is empty, triggers **full withdrawals** - Otherwise triggers partial withdrawals with specified amounts - Excess fee is refunded to `_excessRefundRecipient` ### ejectValidators(bytes \_pubkeys, address \_refundRecipient) ```solidity function ejectValidators(bytes calldata _pubkeys, address _refundRecipient) external payable ``` **Only callable by node operator** (not owner). Triggers full validator exits via EIP-7002 without requiring owner cooperation. This allows node operators to force-exit validators if needed. - If `_refundRecipient` is zero, excess fee refunds go to `msg.sender` - Always triggers full withdrawals ### acceptOwnership() ```solidity function acceptOwnership() public ``` Accepts a pending ownership transfer. Only callable by pending owner. ### transferOwnership(address \_newOwner) ```solidity function transferOwnership(address _newOwner) public onlyOwner ``` Initiates 2-step ownership transfer. ### renounceOwnership() ```solidity function renounceOwnership() public view onlyOwner ``` Blocked by design - always reverts with `RenouncementNotAllowed()`. ### setDepositor(address \_depositor) ```solidity function setDepositor(address _depositor) external onlyOwner ``` Updates depositor address. Reverts if same as current depositor. ### ossify() ```solidity function ossify() external onlyOwner ``` Pins the implementation for this vault (prevents future upgrades). **Warning:** This operation is irreversible. Vault cannot be connected to VaultHub after ossification. ### collectERC20(address \_token, address \_recipient, uint256 \_amount) ```solidity function collectERC20(address _token, address _recipient, uint256 _amount) external onlyOwner ``` Recovers ERC-20 tokens sent to the vault. **Note:** Does not support ETH recovery (reverts with `EthCollectionNotAllowed` if EIP-7528 ETH address is passed). Use `withdraw()` for ETH. ## Receiving ETH The contract has a `receive()` function allowing direct ETH transfers, but the preferred method is `fund()` which emits proper events. ## Related - [VaultHub](/contracts/vault-hub) - [PredepositGuarantee](/contracts/predeposit-guarantee) - [Staking Vault Beacon](/contracts/staking-vault-beacon) - [Staking Vault Factory](/contracts/staking-vault-factory) - [stVaults Technical Design](/run-on-lido/stvaults/tech-documentation/tech-design) --- # UpgradeableBeacon - [Source code](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.2.0/contracts/proxy/beacon/UpgradeableBeacon.sol) - [Deployed contract](https://etherscan.io/address/0x5FbE8cEf9CCc56ad245736D3C5bAf82ad54Ca789) OpenZeppelin UpgradeableBeacon used as the StakingVault beacon. It stores the current StakingVault implementation for all beacon proxies. ## What is UpgradeableBeacon? UpgradeableBeacon is a beacon proxy controller: - holds the current implementation address - allows the Lido DAO to upgrade the implementation if vaults are not ossified - is referenced by all `PinnedBeaconProxy` vaults created by `VaultFactory` ## Ownership and upgrades The beacon is owned by the **Lido DAO** (via the Agent/Aragon governance). This means: - Only Lido DAO governance can upgrade the StakingVault implementation - All beacon proxy vaults automatically use the new implementation after an upgrade - Upgrades affect all vaults simultaneously unless they have been ossified ### Ossification Individual vaults can opt out of future upgrades by calling `ossify()` on their `StakingVault`. This "pins" the vault to its current implementation, making it immune to beacon upgrades. Ossification is: - **Irreversible**: Once ossified, a vault cannot be un-ossified - **Only available after disconnect**: Vaults must disconnect from VaultHub before ossifying - **Owner-controlled**: Only the vault owner can ossify their vault ## View methods ### implementation() ```solidity function implementation() public view returns (address) ``` Returns the current implementation address. ### owner() ```solidity function owner() public view returns (address) ``` Returns the beacon owner. ## Methods ### upgradeTo(address newImplementation) ```solidity function upgradeTo(address newImplementation) public onlyOwner ``` Upgrades the implementation used by the beacon. ### transferOwnership(address newOwner) ```solidity function transferOwnership(address newOwner) public onlyOwner ``` Transfers beacon ownership. ### renounceOwnership() ```solidity function renounceOwnership() public onlyOwner ``` Renounces ownership of the beacon. ## Related - [StakingVault](/contracts/staking-vault) - [Staking Vault Factory](/contracts/staking-vault-factory) --- # VaultFactory - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/VaultFactory.sol) - [Deployed contract](https://etherscan.io/address/0x02Ca7772FF14a9F6c1a08aF385aA96bb1b34175A) Factory for deploying `StakingVault` + `Dashboard` pairs using a beacon proxy. ## What is VaultFactory? VaultFactory creates new stVault instances: - deploys a `PinnedBeaconProxy` pointing to the StakingVault beacon - deploys a `Dashboard` to manage the vault - optionally connects the vault to `VaultHub` VaultHub enforces factory-based deployments upon connection by checking that the vault was created by the current factory or a previous factory in the chain. ## Immutable variables | Variable | Description | | ------------------ | ------------------------------------------------------ | | `LIDO_LOCATOR` | Address of the LidoLocator contract | | `BEACON` | Address of the StakingVault beacon contract | | `DASHBOARD_IMPL` | Address of the Dashboard implementation for cloning | | `PREVIOUS_FACTORY` | Address of the previous factory in the chain (or zero) | ## Factory flows VaultFactory exposes two creation flows: ### 1. Create + connect via `createVaultWithDashboard()` - Requires `msg.value >= CONNECT_DEPOSIT` (1 ETH) - Deploys StakingVault proxy and Dashboard clone - Initializes vault with Dashboard as owner - Initializes Dashboard with **factory as temporary default admin** - Connects to VaultHub with the provided ETH - Grants optional roles (only `_defaultAdmin` sub-roles) - Transfers `DEFAULT_ADMIN_ROLE` to `_defaultAdmin` and revokes from factory ### 2. Create without connecting via `createVaultWithDashboardWithoutConnectingToVaultHub()` - No ETH required - Deploys StakingVault proxy and Dashboard clone - Initializes vault with Dashboard as owner - Initializes Dashboard with `_defaultAdmin` as default admin and **factory as temporary NODE_OPERATOR_MANAGER** - Grants optional roles (only `_nodeOperatorManager` sub-roles) - Transfers `NODE_OPERATOR_MANAGER_ROLE` to `_nodeOperatorManager` and revokes from factory ### Factory chaining The factory supports a `PREVIOUS_FACTORY` immutable reference for migration between factory versions: - VaultHub accepts vaults deployed by the **current factory** or any **previous factory in the chain** - When a new factory is deployed, it references the old factory via `PREVIOUS_FACTORY` - Creates a linked list of valid factories for backwards compatibility - Previously deployed vaults remain eligible for connection without redeployment ``` NewFactory.PREVIOUS_FACTORY โ†’ OldFactory.PREVIOUS_FACTORY โ†’ ... โ†’ address(0) ``` ### Connect deposit When using `createVaultWithDashboard()`, the caller must send at least `CONNECT_DEPOSIT` (1 ETH) with the transaction. This ETH is escrowed by VaultHub and returned when the vault disconnects. ## Structs ### RoleAssignment Role assignment for Dashboard initialization (from `Permissions`): ```solidity struct RoleAssignment { address account; // Account to grant role to bytes32 role; // Role identifier } ``` ## View methods ### deployedVaults(address \_vault) ```solidity function deployedVaults(address _vault) external view returns (bool) ``` Returns whether a vault was deployed by this factory or any previous factory in the chain. Recursively checks `PREVIOUS_FACTORY` if set. ## Methods ### createVaultWithDashboard(...) ```solidity function createVaultWithDashboard( address _defaultAdmin, address _nodeOperator, address _nodeOperatorManager, uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry, Permissions.RoleAssignment[] calldata _roleAssignments ) external payable returns (IStakingVault vault, Dashboard dashboard) ``` Creates a vault + dashboard and connects to VaultHub. **Parameters:** - `_defaultAdmin`: Address to receive `DEFAULT_ADMIN_ROLE` on Dashboard - `_nodeOperator`: Node operator address for the StakingVault - `_nodeOperatorManager`: Address for `NODE_OPERATOR_MANAGER_ROLE` and fee recipient - `_nodeOperatorFeeBP`: Node operator fee in basis points - `_confirmExpiry`: Confirmation expiry time in seconds - `_roleAssignments`: Optional roles to grant (only `_defaultAdmin` sub-roles) **Requirements:** - `msg.value >= CONNECT_DEPOSIT` ### createVaultWithDashboardWithoutConnectingToVaultHub(...) ```solidity function createVaultWithDashboardWithoutConnectingToVaultHub( address _defaultAdmin, address _nodeOperator, address _nodeOperatorManager, uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry, Permissions.RoleAssignment[] calldata _roleAssignments ) external returns (IStakingVault vault, Dashboard dashboard) ``` Creates a vault + dashboard without connecting to VaultHub. **Parameters:** - `_defaultAdmin`: Address to receive `DEFAULT_ADMIN_ROLE` on Dashboard - `_nodeOperator`: Node operator address for the StakingVault - `_nodeOperatorManager`: Address to receive `NODE_OPERATOR_MANAGER_ROLE` and fee recipient - `_nodeOperatorFeeBP`: Node operator fee in basis points - `_confirmExpiry`: Confirmation expiry time in seconds - `_roleAssignments`: Optional roles to grant (only `_nodeOperatorManager` sub-roles) ## Events ```solidity event VaultCreated(address indexed vault); event DashboardCreated(address indexed dashboard, address indexed vault, address indexed admin); ``` ## Related - [StakingVault](/contracts/staking-vault) - [Staking Vault Beacon](/contracts/staking-vault-beacon) - [Dashboard](/contracts/dashboard) - [VaultHub](/contracts/vault-hub) --- # TopUpGateway - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/TopUpGateway.sol) - [Deployed contract](https://etherscan.io/address/0x3FC2C71579D80790Aaa3fc7Be8B66ac39dC57374) - Specification basis: [LIP-35 โ€” Staking Router v3](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-35.md) TopUpGateway is the entry point for topping up `0x02` (compounding, [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251)) Lido Core validators. It verifies each validator's Consensus Layer state on-chain with Merkle proofs, computes per-validator top-up limits, and forwards them to [`StakingRouter.topUp`](/contracts/staking-router#topup), which pulls ether from [Lido](/contracts/lido) and executes the deposits. ## What is TopUpGateway TopUpGateway is the only contract allowed to trigger [validator top-ups](/contracts/staking-router#top-ups) on the [StakingRouter](/contracts/staking-router). Before any ether moves, it: - verifies each validator's container (withdrawal credentials, effective balance, activation/exit epochs, slashed flag) against the Consensus Layer state using a Merkle proof anchored to an [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) beacon block root; - computes a per-validator top-up limit from the configured target balance, the proven effective balance, and the validator's pending deposits; - forwards the validated public keys and limits to the StakingRouter, which determines the exact deposit amounts within these limits. The contract inherits [CLValidatorVerifier](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/CLValidatorVerifier.sol), OpenZeppelin `AccessControlEnumerableUpgradeable`, and [PausableUntil](https://github.com/lidofinance/core/blob/v4.0.0/contracts/common/utils/PausableUntil.sol). It is deployed behind an [OssifiableProxy](/contracts/ossifiable-proxy). ## Top-up flow Top-ups are permissioned: [`topUp`](#topup) is restricted to the `TOP_UP_ROLE`, held by the Lido depositor bot. 1. The depositor bot selects validators of a `0x02` staking module and collects for each: the validator container fields, a Merkle proof anchored to a recent beacon block root, and the balance of the validator's pending deposits. 2. The bot calls [`topUp`](#topup). The gateway validates the request (see [Request processing](#request-processing)) and computes a top-up limit for each validator. 3. The gateway calls [`StakingRouter.topUp`](/contracts/staking-router#topup), which determines the exact per-key amounts within these limits โ€” capped by the [allocation algorithm](/contracts/staking-router#allocation-algorithm) and the global per-block top-up limit โ€” pulls the ether from [Lido](/contracts/lido), and executes the deposits. ```mermaid graph TD; A[Depositor bot] -->|"topUp (CL proofs + pending balances)"| B[TopUpGateway]; C[EIP-4788 beacon roots contract] -.->|beacon block root| B; B -->|"pubkeys + top-up limits"| D[StakingRouter]; E[Lido] -->|depositable ether| D; D -->|top-up deposits| F[DepositContract]; ``` ## Request processing [`topUp`](#topup) accepts a [`TopUpData`](#topupdata) batch targeting a single staking module. Before calling the StakingRouter, the gateway enforces the following: 1. **Access.** The caller holds `TOP_UP_ROLE`, and the contract is not paused. 2. **Well-formedness.** The batch is not empty, all per-validator arrays have the same length, the batch size does not exceed `maxValidatorsPerTopUp`, and `validatorIndices` are strictly increasing (which also rejects duplicates). 3. **Rate limit.** At least `minBlockDistance` blocks have passed since the last effective top-up. 4. **Proof freshness.** The beacon block root is at most `maxRootAge` seconds old and is newer than the last effective top-up, so a Consensus Layer state observed before the previous top-up cannot be replayed. 5. **Module withdrawal credentials.** The target module's withdrawal credentials, fetched from the StakingRouter, are of type `0x02`. 6. **Validator verification.** Each validator's pubkey is 48 bytes, the validator was activated no later than the epoch of the proven slot, and the full validator container is proven against the beacon block root. See [Validator verification](#validator-verification). For each verified validator, the gateway evaluates a top-up limit (see [Top-up limit evaluation](#top-up-limit-evaluation)) and forwards the batch to [`StakingRouter.topUp`](/contracts/staking-router#topup). If at least one limit is non-zero, the gateway records the current block and timestamp for rate limiting; a call where every limit evaluates to zero does not consume the rate limit. ### Validator verification The Merkle proof covers the entire validator container. The gateway builds the expected container leaf using the module's `0x02` withdrawal credentials, so the proof verifies only if the validator's actual withdrawal credentials belong to the protocol โ€” a validator with foreign credentials cannot be topped up. The proof is anchored to a beacon block root read from the [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) beacon roots contract by the supplied `childBlockTimestamp`, and the proven `slot` and `proposerIndex` are bound to the same proof. The verifier is fork-aware: it switches the generalized index of the validators tree at a pivot slot configured at deployment. Pending deposit balances (`pendingBalanceGwei`) are not part of the validator container and cannot be proven this way; they are supplied by the caller, which is one more reason `topUp` is restricted to the trusted depositor bot. ### Top-up limit evaluation The per-validator limit is derived from two configurable parameters โ€” `targetBalanceGwei`, the validator balance ceiling after a top-up, and `minTopUpGwei`, the minimum top-up worth performing: - the limit is zero if the validator is exiting or exited (`exitEpoch` is set) or slashed; - the limit is zero if `effectiveBalance + pendingBalanceGwei โ‰ฅ targetBalanceGwei`; - otherwise, the limit is `targetBalanceGwei โˆ’ (effectiveBalance + pendingBalanceGwei)`, zeroed if below `minTopUpGwei`. Pending deposits are counted so that deposits already in flight in the Consensus Layer deposit queue are not doubled by a new top-up. The resulting limits are ceilings, not exact amounts: the StakingRouter and the staking module determine the exact per-key deposit amounts within them. ## Roles Access to lever methods is restricted using the functionality of the OpenZeppelin `AccessControlEnumerable` contract: - `DEFAULT_ADMIN_ROLE` โ€” manages role assignments; - `TOP_UP_ROLE` โ€” allows submitting top-ups; held by the Lido depositor bot; - `MANAGE_LIMITS_ROLE` โ€” allows updating the gateway parameters: batch size, block distance, root age, and balance limits; - `PAUSE_ROLE` / `RESUME_ROLE` โ€” allow pausing and resuming the contract (see [PausableUntil](https://github.com/lidofinance/core/blob/v4.0.0/contracts/common/utils/PausableUntil.sol)). ## Structs ### TopUpData A top-up batch targeting a single staking module. The `keyIndices`, `operatorIds`, `validatorWitness`, and `pendingBalanceGwei` arrays are aligned by position to `validatorIndices[i]`, and `validatorIndices` must be sorted in strictly ascending order. ```solidity struct TopUpData { uint256 moduleId; // target staking module id uint256[] keyIndices; // key indices within the module uint256[] operatorIds; // node operator ids within the module uint256[] validatorIndices; // validator indices on the Consensus Layer BeaconRootData beaconRootData; // beacon block root reference for the proofs ValidatorWitness[] validatorWitness; // validator containers with Merkle proofs uint256[] pendingBalanceGwei; // pending deposits per validator, in gwei } ``` ### BeaconRootData Reference to the beacon block root all proofs in the batch are anchored to. ```solidity struct BeaconRootData { uint64 childBlockTimestamp; // EL block timestamp for the EIP-4788 lookup uint64 slot; // beacon block header slot uint64 proposerIndex; // beacon block header proposer index } ``` ### ValidatorWitness Full validator container fields (except withdrawal credentials, which are derived from the module) with a Merkle proof of the container's inclusion in the Beacon state. ```solidity struct ValidatorWitness { bytes32[] proofValidator; // Merkle path: Validator[i] โ†’ state_root โ†’ beacon block root bytes pubkey; uint64 effectiveBalance; uint64 activationEligibilityEpoch; uint64 activationEpoch; uint64 exitEpoch; uint64 withdrawableEpoch; bool slashed; } ``` ## View methods ### `getLastTopUpTimestamp` Returns the timestamp of the last effective top-up (one with a non-zero total limit). ```solidity function getLastTopUpTimestamp() external view returns (uint256); ``` ### `getMaxValidatorsPerTopUp` Returns the maximum number of validators allowed per `topUp` call. ```solidity function getMaxValidatorsPerTopUp() external view returns (uint256); ``` ### `getMinBlockDistance` Returns the minimum number of blocks that must pass between effective top-ups. ```solidity function getMinBlockDistance() external view returns (uint256); ``` ### `isBlockDistancePassed` Returns true if enough blocks have passed since the last top-up, or if no top-up has happened yet. ```solidity function isBlockDistancePassed() external view returns (bool); ``` ### `getMaxRootAge` Returns the maximum age (in seconds) of the beacon block root relative to the current block timestamp. ```solidity function getMaxRootAge() external view returns (uint256); ``` ### `getTargetBalanceGwei` Returns the target validator balance ceiling after a top-up, in gwei. ```solidity function getTargetBalanceGwei() external view returns (uint256); ``` ### `getMinTopUpGwei` Returns the minimum top-up that can be performed, in gwei. ```solidity function getMinTopUpGwei() external view returns (uint256); ``` ## Write methods ### `topUp` Verifies the batched validators against the Consensus Layer state, evaluates per-validator top-up limits, and calls [`StakingRouter.topUp`](/contracts/staking-router#topup). See [Request processing](#request-processing) for the detailed flow. Restricted to the `TOP_UP_ROLE` role, held by the Lido depositor bot. ```solidity function topUp(TopUpData calldata _topUps) external; ``` **Parameters:** | Name | Type | Description | | --------- | ----------- | ------------------------------------------------------------------------------------------------------- | | `_topUps` | `TopUpData` | top-up batch: validator containers, pending deposit balances, and Merkle proofs (see [TopUpData](#topupdata)) | **Reverts:** - if the caller does not have the `TOP_UP_ROLE` or the contract is paused; - with `WrongArrayLength` if `validatorIndices` is empty or any per-validator array has a different length; - with `MaxValidatorsPerTopUpExceeded` if the batch exceeds `maxValidatorsPerTopUp`; - with `InvalidValidatorIndicesSortOrder` if `validatorIndices` is not strictly increasing; - with `MinBlockDistanceNotMet` if fewer than `minBlockDistance` blocks have passed since the last top-up; - with `RootIsTooOld` if the beacon root is older than `maxRootAge`, and with `RootPrecedesLastTopUp` if it is not newer than the last top-up; - with `WrongWithdrawalCredentials` if the module's withdrawal credentials are not of type `0x02`; - with `WrongPubkeyLength` if any pubkey is not 48 bytes, and with `ValidatorIsNotActivated` if any validator's activation epoch is later than the proven slot's epoch; - if any validator's Merkle proof fails verification. ### `setMaxValidatorsPerTopUp` Sets the maximum number of validators per `topUp` call. The value must be non-zero and fit into `uint64`. Restricted to the `MANAGE_LIMITS_ROLE` role. ```solidity function setMaxValidatorsPerTopUp(uint256 _newValue) external; ``` ### `setMinBlockDistance` Sets the minimum number of blocks between effective top-ups. The value must be non-zero and fit into `uint16`. Restricted to the `MANAGE_LIMITS_ROLE` role. ```solidity function setMinBlockDistance(uint256 _newValue) external; ``` ### `setMaxRootAge` Sets the maximum allowed age of the beacon block root relative to the current block timestamp, in seconds. The value must be non-zero and fit into `uint16`. Restricted to the `MANAGE_LIMITS_ROLE` role. ```solidity function setMaxRootAge(uint256 _newValue) external; ``` ### `setTopUpBalanceLimits` Sets the [top-up balance limits](#top-up-limit-evaluation). Both values must be non-zero and fit into `uint64`; reverts with `MinTopUpExceedsTarget` if `_minTopUpGwei` exceeds `_targetBalanceGwei`. Restricted to the `MANAGE_LIMITS_ROLE` role. ```solidity function setTopUpBalanceLimits(uint256 _targetBalanceGwei, uint256 _minTopUpGwei) external; ``` **Parameters:** | Name | Type | Description | | -------------------- | --------- | --------------------------------------------------------- | | `_targetBalanceGwei` | `uint256` | target validator balance ceiling after a top-up, in gwei | | `_minTopUpGwei` | `uint256` | minimum top-up that can be performed, in gwei | ### `pauseFor` Pauses the contract for the specified duration, blocking new top-ups. Restricted to the `PAUSE_ROLE` role. ```solidity function pauseFor(uint256 _duration) external; ``` **Parameters:** | Name | Type | Description | | ----------- | --------- | ---------------------------------------------------------------- | | `_duration` | `uint256` | pause duration in seconds (use `PAUSE_INFINITELY` for unlimited) | ### `pauseUntil` Pauses the contract until the specified timestamp (inclusive). Restricted to the `PAUSE_ROLE` role. ```solidity function pauseUntil(uint256 _pauseUntilInclusive) external; ``` **Parameters:** | Name | Type | Description | | ---------------------- | --------- | ----------------------------------------- | | `_pauseUntilInclusive` | `uint256` | the last second to pause until, inclusive | ### `resume` Resumes the contract. Restricted to the `RESUME_ROLE` role. ```solidity function resume() external; ``` ## Events ### `LastTopUpChanged` Emitted after an effective top-up (one with a non-zero total limit) with the current block timestamp. ```solidity event LastTopUpChanged(uint256 newValue); ``` ### `MaxValidatorsPerTopUpChanged` ```solidity event MaxValidatorsPerTopUpChanged(uint256 newValue); ``` ### `MinBlockDistanceChanged` ```solidity event MinBlockDistanceChanged(uint256 newValue); ``` ### `MaxRootAgeChanged` ```solidity event MaxRootAgeChanged(uint256 newValue); ``` ### `TopUpBalanceLimitsChanged` ```solidity event TopUpBalanceLimitsChanged(uint256 targetBalanceGwei, uint256 minTopUpGwei); ``` --- # TriggerableWithdrawalsGateway - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/TriggerableWithdrawalsGateway.sol) - [Deployed contract](https://etherscan.io/address/0xDC00116a0D3E064427dA2600449cfD2566B3037B) - Specification basis: [LIP-30 โ€” Triggerable withdrawals](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-30.md) ## What is TriggerableWithdrawalsGateway TriggerableWithdrawalsGateway (TWG) is a gateway contract introducing an validators' execution triggerable exit path for the Lido protocol. It proxies exit request calls to the WithdrawalVault, checking permissions, applying limits, and refunding any redundant trigger fee costs. ## Roles and access control Access to lever methods is restricted using the functionality of the [AccessControlEnumerable](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/utils/access/AccessControlEnumerable.sol) contract and a bunch of [granular roles](#permissions). To engage Emergency Brakes and unpause if required, it inherits from `PausableContract` (see [CircuitBreaker](/contracts/circuit-breaker)). ## Methods ### `ADD_FULL_WITHDRAWAL_REQUEST_ROLE` An ACL role granting the permission to submit exit requests. ```solidity bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); ``` ### `TW_EXIT_LIMIT_MANAGER_ROLE` An ACL role granting the permission to modify limit params. ```solidity bytes32 public constant TW_EXIT_LIMIT_MANAGER_ROLE = keccak256("TW_EXIT_LIMIT_MANAGER_ROLE"); ``` ### `getExitRequestLimitFullInfo` Returns information about current limits data. ```solidity function getExitRequestLimitFullInfo() external view; ``` **Returns:** | Name | Type | Description | | --------------------------- | --------- | ----------------------------------------------------------------------------------------- | | `_maxExitRequestsLimit` | `uint256` | Maximum exit requests limit | | `_exitsPerFrame` | `uint256` | The number of exits that can be restored per frame | | `_frameDurationInSec` | `uint256` | The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored | | `_prevExitRequestsLimit` | `uint256` | Limit left after previous requests | | `_currentExitRequestsLimit` | `uint256` | Current exit requests limit | ### `setExitRequestLimit` Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. ```solidity function setExitRequestLimit( uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec ) external onlyRole(TW_EXIT_LIMIT_MANAGER_ROLE); ``` **Parameters:** | Name | Type | Description | | ---------------------- | --------- | ------------------------------------------------------------------------------------------ | | `maxExitRequestsLimit` | `uint256` | The maximum number of exit requests. | | `exitsPerFrame` | `uint256` | The number of exits that can be restored per frame. | | `frameDurationInSec` | `uint256` | The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. | ### `triggerFullWithdrawals` Submits Triggerable Withdrawal Requests to the Withdrawal Vault as full withdrawal requests for the specified validator public keys. ```solidity function triggerFullWithdrawals( IStakingRouter.ValidatorExitData[] calldata validatorsData, address refundRecipient, uint256 exitType, ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance whenResumed ``` **Structures**: ```solidity struct ValidatorExitData { uint256 stakingModuleId; uint256 nodeOperatorId; bytes pubkey; } ``` **Parameters:** | Name | Type | Description | | ----------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `validatorsData` | `ValidatorExitData[]` | An array of `ValidatorExitData` structs, each representing a validator for which a withdrawal request will be submitted. | | `refundRecipient` | `address` | The address that will receive any excess ETH sent for fees. | | `exitType` | `uint256` | A parameter indicating the type of exit, passed to the Staking Module. | ## Permissions ### ADD_FULL_WITHDRAWAL_REQUEST_ROLE() An ACL role granting the permission to add withdrawal requests. ```solidity bytes32 public constant RESUME_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); ``` ### TW_EXIT_LIMIT_MANAGER_ROLE() An ACL role granting the permission to change trigger request limits. ```solidity bytes32 public constant TW_EXIT_LIMIT_MANAGER_ROLE = keccak256("TW_EXIT_LIMIT_MANAGER_ROLE"); ``` --- # TRP VestingEscrow - [Source Code](https://github.com/lidofinance/lido-vesting-escrow/tree/main/contracts) - Deployed Contracts (mainnet) - [VestingEscrowFactory](https://etherscan.io/address/0xDA1DF6442aFD2EC36aBEa91029794B9b2156ADD0) - [VestingEscrowProto](https://etherscan.io/address/0x484FD04c598A095360DF89bF85AB34c37127AA39) - [VotingAdapter](https://etherscan.io/address/0xCFda8aB0AE5F4Fa33506F9C51650B890E4871Cc1) - [Detailed contracts spec](https://hackmd.io/@lido/rkKpFX8So) [Token Reward Program (TRP)](https://research.lido.fi/t/lidodao-token-rewards-plan-trp/3364) escrow contracts allow transparent on-chain distribution and vesting of the token rewards for the Lido DAO contributors. ## VestingEscrowFactory ### Public variables - `voting_adapter: address` - address of the VotingAdapter used in the vestings - `owner: address` - factory and vestings owner - `manager: address` - vestings manager ### View methods #### target() Returns immutable `TARGET` ```vyper @external @view def target() -> uint256 ``` #### token() Returns immutable `TOKEN` ```vyper @external @view def token() -> uint256 ``` ### Methods #### deploy_vesting_contract() :::note Before calling `deploy_vesting_contract()` caller need to have enough tokens on the balance and call `approve(vestingFactoryAddress, fundAmount)` on the token contract ::: Deploy and fund a new instance of the `VestingEscrow` for the given `recipient`. Set all params for the deployed escrow. Returns address of the deployed escrow ```vyper @external def deploy_vesting_contract( amount: uint256, recipient: address, vesting_duration: uint256, vesting_start: uint256 = block.timestamp, cliff_length: uint256 = 0, is_fully_revokable: bool = False ) -> address ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|------------------------------------------------------------| | `amount` | `uint256` | Amount of the tokens to be controlled by vesting | | `recipient` | `address` | Recipient of the vested funds | | `vesting_duration` | `uint256` | Vesting duration in seconds | | `vesting_start` | `uint256` | Vesting start time in seconds (unix time in sec) | | `cliff_length` | `uint256` | Cliff duration in seconds | | `is_fully_revokable` | `bool` | Flag that enables `revoke_all` method | :::note Reverts if any of the following is true: - `vesting_duration <= 0`. - `cliff_length >= vesting_duration` - token transfer from caller to factory fails - approve of the tokens to the actual vesting fails ::: #### recover_erc20() Collect ERC20 tokens from the contract to the `owner`. ```vyper @external def recover_erc20( token: address, amount: uint256 ) ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `token` | `address` | Address of ERC20 token to recover | | `amount` | `uint256` | Amount of the tokens to recover | :::note Reverts if: - tokens transfer to `owner` fails ::: #### recover_ether() Collect all ether from the contract to the `owner`. ```vyper @external def recover_ether() ``` :::note Reverts if: - Ether transfer to `owner` fails ::: #### update_voting_adapter() Set `self.voting_adapter` to `voting_adapter`. ```vyper @external def update_voting_adapter( voting_adapter: address ) ``` ##### Parameters | Name | Type | Description | |------------------|-----------------------------|------------------------------------------------------------| | `voting_adapter` | `address` | New voting adapter | :::note Reverts if: - called by anyone except `VestingEscrowFactory` owner ::: #### change_owner() Set `self.owner` to `owner`. ```vyper @external def change_owner( owner: address ) ``` ##### Parameters | Name | Type | Description | |------------------|-----------------------------|------------------------------------------------------------| | `owner` | `address` | New `owner` address | :::note Reverts if: - called by anyone except `VestingEscrowFactory` owner - arg `owner` is empty address ::: #### change_manager() Set `self.manager` to `manager`. ```vyper @external def change_manager( manager: address ) ``` ##### Parameters | Name | Type | Description | |------------------|-----------------------------|------------------------------------------------------------| | `manager` | `address` | New `manager` address | :::note Reverts if: - called by anyone except `VestingEscrowFactory` owner ::: ## VestingEscrow ### Public variables - `recipient: address` - address that can claim tokens from escrow - `token: ERC20` - address of the vested token - `start_time: uint256` - vesting start time (UTC time in UNIX seconds) - `end_time: uint256` - vesting end time (UTC time in UNIX seconds) - `cliff_length: uint256` - cliff length in seconds - `factory: IVestingEscrowFactory` - address of the parent factory - `total_locked: uint256` - total amount of the tokens to be vested (does not change after claims) - `is_fully_revokable: bool` - flag showing if the escrow is fully revocable or not - `total_claimed: uint256` - total amount of the claimed tokens - `disabled_at: uint256` - effective vesting end time (UTC time in UNIX seconds). Can differ from end_time in case of the revoke_xxx methods call - `initialized: bool` - flag indicating that escrow was initialized - `is_fully_revoked: bool` - flag indicating that escrow was fully revoked and there are no more tokens ### View methods #### unclaimed() Returns the current amount of the tokens available for the claim. ```vyper @external @view def unclaimed() -> uint256 ``` #### locked() Returns the current amount of the tokens locked. ```vyper @external @view def locked() -> uint256 ``` ### Methods #### claim() Claim tokens to the `beneficiary` address. If the requested amount is larger than `unclaimed`, then the `unclaimed` amount will be claimed. Returns actual amount of the tokens claimed. ```vyper @external def claim( beneficiary: address = msg.sender, amount: uint256 = max_value(uint256) ) ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `beneficiary` | `address` | Address to claim tokens to | | `amount` | `uint256` | Amount of the tokens to claim | :::note Reverts if: - called by anyone except vesting `recipient` - tokens transfer to `beneficiary` fails ::: #### revoke_unvested() Disable further flow of tokens and revoke the unvested part to the owner. ```vyper @external def revoke_unvested() ``` :::note Reverts if: - called by anyone except `VestingEscrowFactory` owner or manager - tokens transfer to `VestingEscrowFactory.owner()` fails ::: #### revoke_all() Disable further flow of tokens and revoke all tokens to the owner. ```vyper @external def revoke_all() ``` :::note Reverts if: - `is_fully_revocable` param of the `VestingEscrow` is not True - called by anyone except `VestingEscrowFactory` owner - tokens transfer to `VestingEscrowFactory.owner` fails ::: #### recover_erc20() Collect ERC20 tokens from the contract to the `recipient`. ```vyper @external def recover_erc20( token: address, amount: uint256 ) ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `token` | `address` | Address of ERC20 token to recover | | `amount` | `uint256` | Amount of the tokens to recover | :::note Reverts if: - tokens transfer to `recipient` fails ::: #### recover_ether() Collect all ether from the contract to the `recipient`. ```vyper @external def recover_ether() ``` :::note Reverts if: - Ether transfer to `recipient` fails ::: #### aragon_vote() Participate in the Aragon vote using all available tokens on the contract's balance. Uses `delegateCall` to `VotingAdapter`. `VotingAdapter` address is fetched from `self.factory`. ```vyper @external def aragon_vote( abi_encoded_params: Bytes[1000] ) ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|------------------------------------------------------------------------------------------------------------------| | `abi_encoded_params` | `Bytes[1000]` | ABI encoded params for the `vote` method call. can be compiled using `VotingAdapter.encode_aragon_vote_calldata` | :::note Reverts if: - called by anyone except vesting `recipient` ::: #### snapshot_set_delegate() Delegate Snapshot voting power of all available tokens on the contract's balance to `delegate`. Uses `delegateCall` to `VotingAdapter`. `VotingAdapter` address is fetched from `self.factory`. ```vyper @external def snapshot_set_delegate( abi_encoded_params: Bytes[1000] ) ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------| | `abi_encoded_params` | `Bytes[1000]` | ABI encoded params for the `delegate` method call. can be compiled using `VotingAdapter.encode_snapshot_set_delegate_calldata` | :::note Reverts if: - called by anyone except vesting `recipient` ::: #### delegate() :::note Stub at the moment of writing ::: Delegate voting power of all available tokens on the contract's balance to `delegate`. Uses `delegateCall` to VotingAdapter. `VotingAdapter` address is fetched from `self.factory`. ```vyper @external def delegate( abi_encoded_params: Bytes[1000] ) ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------| | `abi_encoded_params` | `Bytes[1000]` | ABI encoded params for the `vote` method call. can be compiled using `VotingAdapter.encode_delegate_calldata` | :::note Reverts if: - called by anyone except vesting `recipient` ::: ## VotingAdapter ### Public variables - `owner: address` - votingAdapter owner ### View methods #### encode_aragon_vote_calldata() Returns abi encoded params for the `aragon_vote` call. ```vyper @external @view def encode_aragon_vote_calldata( voteId: uint256, supports: bool ) -> Bytes[1000] ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `voteId` | `uint256` | Aragon vote id | | `amount` | `bool` | Supports flag. `True` - for, `False` - against | #### encode_snapshot_set_delegate_calldata() Returns abi encoded params for the `snapshot_set_delegate` call. ```vyper @external @view def encode_snapshot_set_delegate_calldata( delegate: address ) -> Bytes[1000] ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `delegate` | `address` | Address to delegate snapshot voting power to | #### encode_delegate_calldata() Returns abi encoded params for the `delegate` call. ```vyper @external @view def encode_delegate_calldata( delegate: address ) -> Bytes[1000] ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `delegate` | `address` | Address to delegate voting power to | ### Methods #### aragon_vote() Participate in the Aragon vote using all available tokens on the contract's balance. It makes sense only for delegateCalls, so the caller's balance will be used. Uses `VOTING_CONTRACT_ADDR` as the voting contract address. ```vyper @external def aragon_vote( abi_encoded_params: Bytes[1000] ) ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|------------------------------------------------------------------------------------------------------------------| | `abi_encoded_params` | `Bytes[1000]` | ABI encoded params for the `vote` method call. can be compiled using `VotingAdapter.encode_aragon_vote_calldata` | :::note Reverts if: - called by anyone except vesting `recipient` ::: #### snapshot_set_delegate() Delegate Snapshot voting power of all available tokens. Makes sense only for delegateCalls so that the balance of the caller will be used. Uses `SNAPSHOT_DELEGATE_CONTRACT_ADDR` as the voting contract address. ```vyper @external def snapshot_set_delegate( abi_encoded_params: Bytes[1000] ) ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------| | `abi_encoded_params` | `Bytes[1000]` | ABI encoded params for the `delegate` method call. can be compiled using `VotingAdapter.encode_snapshot_set_delegate_calldata` | :::note Reverts if: - called by anyone except vesting `recipient` ::: #### delegate() :::note Stub at the moment of writing ::: Stub for the future implementation of the Voting with Delegation. ```vyper @external def delegate( abi_encoded_params: Bytes[1000] ) ``` ##### Parameters | Name | Type | Description | |----------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------| | `abi_encoded_params` | `Bytes[1000]` | ABI encoded params for the `vote` method call. can be compiled using `VotingAdapter.encode_delegate_calldata` | :::note Always reverts ::: #### recover_erc20() Collect ERC20 tokens from the contract to the `owner`. ```vyper @external def recover_erc20( token: address, amount: uint256 ) ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `token` | `address` | Address of ERC20 token to recover | | `amount` | `uint256` | Amount of the tokens to recover | :::note Reverts if: - tokens transfer to `owner` fails ::: #### recover_ether() Collect all ether from the contract to the `owner`. ```vyper @external def recover_ether() ``` :::note Reverts if: - Ether transfer to `owner` fails ::: #### change_owner() Set `self.owner` to `owner` ```vyper @external def change_owner( owner: address ) ``` ##### Parameters | Name | Type | Description | |----------------|-----------------------------|------------------------------------------------------------| | `owner` | `address` | New `owner` address | :::note Reverts if: - called by anyone except `VotingAdapter` owner - arg `owner` is empty address ::: --- # ValidatorConsolidationRequests - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol) - [Deployed contract](https://etherscan.io/address/0xaC4Aae7123248684C405A4b0038C1560EC7fE018) Helper contract that builds calldata for validator consolidation requests (EIP-7251) into stVaults and exposes the current consolidation fee. ## What is ValidatorConsolidationRequests? This contract is used by the stVaults CLI to prepare consolidation requests with on-chain validation and fee calculation. It is designed for: - **Batched execution** via EIP-5792 - **Integration with Vault CLI tooling** - **Fee exemption support** to prevent consolidated balances from being counted as rewards The contract is scoped to incoming consolidations into stVaults and is not used for Lido Core or other consolidation flows. ### Required permissions The caller must: 1. Have their address set as withdrawal credentials for the source validators being consolidated 2. Have the `NODE_OPERATOR_FEE_EXEMPT_ROLE` role assigned in the Dashboard (for fee exemption calls) ### Why fee exemption? Node operator fees are applied only on **rewards**, defined as "all external ether that appeared in the vault on top of the initially deposited one". Without fee exemption, consolidated validator balances would incorrectly be included in the rewards base, leading to overcharging. By passing the sum of all source validator balances, you ensure these balances are excluded from the reward calculation. :::warning This is not a precise method. It does not account for future rewards that consolidated validators may earn after the call, so in some setups additional correction may be required. ::: ## Constants | Constant | Value | Description | | ----------------------------------------- | -------------------------------------------- | ---------------------------------------------------- | | `CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS` | `0x0000BBdDc7CE488642fb579F8B00f3a590007251` | EIP-7251 consolidation requests contract | | `PUBLIC_KEY_LENGTH` | 48 | Length of validator public key in bytes | | `CONSOLIDATION_REQUEST_CALLDATA_LENGTH` | 96 | Length of consolidation request calldata (2 pubkeys) | | `MINIMUM_VALIDATOR_BALANCE` | 16 ether | Minimum balance per validator for validation | ## Immutable variables | Variable | Description | | -------------- | ----------------------------------- | | `LIDO_LOCATOR` | Address of the LidoLocator contract | ## View methods ### getConsolidationRequestsAndFeeExemptionEncodedCalls(...) ```solidity function getConsolidationRequestsAndFeeExemptionEncodedCalls( bytes[] calldata _sourcePubkeys, bytes[] calldata _targetPubkeys, address _dashboard, uint256 _allSourceValidatorBalancesWei ) external view returns ( bytes memory feeExemptionEncodedCall, bytes[] memory consolidationRequestEncodedCalls ) ``` Returns encoded calldata for fee exemption and consolidation requests. **Parameters:** | Parameter | Description | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `_sourcePubkeys` | Array of tightly packed 48-byte public keys to consolidate **from**. Each array element can contain multiple concatenated pubkeys. | | `_targetPubkeys` | Array of single 48-byte public keys to consolidate **to**. Must match length of `_sourcePubkeys`. | | `_dashboard` | Dashboard contract address (must be owner of the connected vault) | | `_allSourceValidatorBalancesWei` | Total balance (wei) of all source validators for fee exemption | **Returns:** - `feeExemptionEncodedCall`: Encoded call to `addFeeExemption()` (empty if `_allSourceValidatorBalancesWei` is zero) - `consolidationRequestEncodedCalls`: Array of encoded calls for EIP-7251 consolidation requests **Validations performed:** - Source and target pubkey arrays must have equal length - Vault must be connected to VaultHub and not pending disconnect - Dashboard must be the owner of the staking vault - Source pubkeys must be properly formatted (multiples of 48 bytes) - Target pubkeys must be exactly 48 bytes each - If non-zero, `_allSourceValidatorBalancesWei` must be at least 16 ETH per consolidation request **Recommended usage:** Call this function via the Vault CLI using WalletConnect signing. The CLI performs pre-checks of source and target validator states, verifies withdrawal credential prefixes, calculates current validator balances, generates request calldata, and submits batched transactions via EIP-5792. ### getConsolidationRequestFee() ```solidity function getConsolidationRequestFee() external view returns (uint256) ``` Returns the current EIP-7251 consolidation request fee. This value is used by the CLI when building the fee-exemption flow for Dashboard. Note: the fee is only valid for the current block and may change in subsequent blocks. ## Related - [Consolidations guide](/run-on-lido/stvaults/tech-documentation/consolidation) - [Dashboard](/contracts/dashboard) - [NodeOperatorFee](/contracts/dashboard#node-operator-fee-accounting) --- # ValidatorExitDelayVerifier - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/ValidatorExitDelayVerifier.sol) - [Deployed contract](https://etherscan.io/address/0xbDb567672c867DB533119C2dcD4FB9d8b44EC82f) ## What is ValidatorExitDelayVerifier `ValidatorExitDelayVerifier` is a helper contract that accepts reports about validators that have not started to exit within the allowed window after being requested to exit via the Validators Exit Bus. It verifies basic preconditions and forwards accepted reports into the staking modules through the `StakingRouter`. In short: when a validator remains eligible to exit for too long (exceeds a node-operator threshold), the contract accepts a proof and calls `StakingRouter.reportValidatorExitDelay(...)`. The data is then propagated to the respective staking module (e.g., Curated, Simple DVT, CSM) where exit-delay penalties may be applied per module rules. ## Methods ### verifyValidatorExitDelay Verifies that the provided validators were not requested to exit on the CL after a VEB exit request. Reports exit delays to the Staking Router. ```solidity function verifyValidatorExitDelay( ProvableBeaconBlockHeader calldata beaconBlock, ValidatorWitness[] calldata validatorWitnesses, ExitRequestData calldata exitRequests ) external; ``` #### Parameters | Name | Type | Description | | -------------------- | --------------------------- | ---------------------------------------------------------------------------------- | | `beaconBlock` | `ProvableBeaconBlockHeader` | Header of the beacon block root with block timestamp | | `validatorWitnesses` | `ValidatorWitness[]` | Validators' state at the specified slot with proofs from the EL and CL | | `exitRequests` | `ExitRequestData` | Data submitted to the VEB. Used to verify that the validator was requested to exit | ### verifyHistoricalValidatorExitDelay Verifies that the provided validators were not requested to exit on the CL after a VEB exit request. Reports exit delays to the Staking Router. Contains additional proof for historical blocks. ```solidity function verifyHistoricalValidatorExitDelay( ProvableBeaconBlockHeader calldata beaconBlock, HistoricalHeaderWitness calldata oldBlock, ValidatorWitness[] calldata validatorWitnesses, ExitRequestData calldata exitRequests ) external; ``` | Name | Type | Description | | -------------------- | --------------------------- | ---------------------------------------------------------------------------------------- | | `beaconBlock` | `ProvableBeaconBlockHeader` | Header of the beacon block root with block timestamp | | `oldBlock` | `HistoricalHeaderWitness` | Historical block header witness data and its proof | | `validatorWitnesses` | `ValidatorWitness[]` | Array of validator witnesses confirming they have not yet exited in `beaconBlock.header` | | `exitRequests` | `ExitRequestData` | Data submitted to the VEB. Used to verify that validator was requested to exit | --- # ValidatorsExitBusOracle - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol) - [Deployed contract](https://etherscan.io/address/0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e) - Inherits [ValidatorsExitBus](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/oracle/ValidatorsExitBus.sol) - Inherits [BaseOracle](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/oracle/BaseOracle.sol) :::info It's advised to read [What is Lido Oracle mechanism](/guides/oracle-operator-manual#intro) before ::: ## What is ValidatorsExitBusOracle A contract that implements an on-chain "source of truth" message bus between the protocol's off-chain oracle and off-chain observers, with the main goal of delivering validator exit requests to the Lido-participating node operators. The oracle report determines which validators should be requested to exit to satisfy withdrawal queue demand, following the policy and prioritization rules described in the [Validator Exits and Penalties](/guides/oracle-spec/penalties) page. :::note Placed exit requests via `ValidatorsExitBusOracle` should be processed timely according to the ratified Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)). ::: Access to privileged methods is restricted using the functionality of the [AccessControlEnumerable](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/utils/access/AccessControlEnumerable.sol) contract and a bunch of [granular roles](#permissions). ## Report cycle The oracle work is delineated by equal time periods called frames. In normal operation, oracles finalize a report in each frame (the frame duration is 75 Ethereum Consensus Layer epochs, each frame starts at ~04:00, ~12:00, ~20:00 UTC). Each frame has a reference slot and processing deadline. Report data is gathered by looking at the world state (both Ethereum Execution and Consensus Layers) at the moment of the frame's reference slot (including any state changes made in that slot), and must be processed before the frame's processing deadline. Reference slot for each frame is set to the last slot of the epoch preceding the frame's first epoch. The processing deadline is set to the last slot of the last epoch of the frame. It's worth noting that frame length [can be changed](/contracts/hash-consensus#setframeconfig). And if oracle report is delayed it does not extend the report period, unless it's missed. In this case, the next report will have the report period increased. The frame includes these stages: - **Waiting** - oracle starts as a [daemon](/guides/oracle-operator-manual#the-oracle-daemon) and wakes up every 12 seconds (by default) in order to find the last finalized slot, trying to collate it with the expected reference slot; - **Data collection**: oracles monitor the state of both the execution and consensus layers and collect the data for the successfully arrived finalized reference slot; - **Hash consensus**: oracles analyze the report data, compile the report and submit its hash to the [HashConsensus](/contracts/hash-consensus) smart contract; - **Core update report**: once the [quorum](/contracts/hash-consensus#getquorum) of hashes is reached, meaning more than half of the oracles submitted the same hash (i.e., 5 of 9 oracle committee members at the moment of writing), one of the oracles chosen in turn submits the actual report to the `ValidatorsExitBusOracle` contract, which triggers a chain of the [`ValidatorExitRequest`](#validatorexitrequest) events containing details about the next validators to be ejected (to initiate a voluntary exit from the Ethereum Consensus Layer side). ## Report data The function `submitReportData()` accepts the following `ReportData` structure. ```solidity struct ReportData { uint256 consensusVersion; uint256 refSlot; uint256 requestsCount; uint256 dataFormat; bytes data; } ``` **Oracle consensus info** - `consensusVersion` โ€” Version of the oracle consensus rules. A current version expected by the oracle can be obtained by calling `getConsensusVersion()`. - `refSlot` โ€” Reference slot for which the report was calculated. The state being reported must include all state changes resulting from the all blocks up to this reference slot (inclusive). The epoch containing the slot must be finalized prior to calculating the report. **Requests data** - `requestsCount` โ€” Total number of validator exit requests in this report. Must match the number of requests packed into `data`. - `dataFormat` โ€” Format of the validator exit requests data. For oracle reports, only the `DATA_FORMAT_LIST_WITH_KEY_INDEX=2` value is supported. - `data` โ€” Validator exit requests data. Can differ based on the data format, see the constant defining a specific data format [here](#data_format_list_with_key_index) for more info. The report is sanity-checked by the [OracleReportSanityChecker](/contracts/oracle-report-sanity-checker) contract: the upper-bound total effective balance of the validators referenced in the report โ€” 32 ETH per validator for modules with `0x01`-type withdrawal credentials and 2048 ETH per validator for modules with `0x02`-type withdrawal credentials โ€” must not exceed the limit enforced by `OracleReportSanityChecker.checkExitBusOracleReport`. ## Constants ### DATA_FORMAT_LIST() The list format of the validator exit requests data. Accepted for exit requests data submitted by trusted entities (e.g., Easy Track for the Curated and SDVT modules) via [`submitExitRequestsData`](#submitexitrequestsdata). Oracle reports use [`DATA_FORMAT_LIST_WITH_KEY_INDEX`](#data_format_list_with_key_index). :::note Each validator exit request is described by the following 64-byte array: ``` MSB <------------------------------------------------------- LSB | 3 bytes | 5 bytes | 8 bytes | 48 bytes | | moduleId | nodeOpId | validatorIndex | validatorPubkey | ``` All requests are tightly packed into a byte array where requests follow one another without any separator or padding, and passed to the `data` field of the report structure. Requests must be sorted in the ascending order by the following compound key: `(moduleId, nodeOpId, validatorIndex)`. ::: ```solidity uint256 public constant DATA_FORMAT_LIST = 1; ``` ### DATA_FORMAT_LIST_WITH_KEY_INDEX() The extended list format of the validator exit requests data that includes a key index for each validator. The `keyIndex` is used to validate the pubkey against the keys registered in the staking module, which also allows the key type (`0x01`/`0x02`) to be determined on-chain. Oracle reports submitted via [`submitReportData`](#submitreportdata) must use this format. :::note Each validator exit request is described by the following 72-byte array: ``` MSB <-------------------------------------------------------------------- LSB | 3 bytes | 5 bytes | 8 bytes | 8 bytes | 48 bytes | | moduleId | nodeOpId | validatorIndex | keyIndex | validatorPubkey | ``` All requests are tightly packed into a byte array where requests follow one another without any separator or padding, and passed to the `data` field of the report structure. Requests must be sorted in the ascending order by the following compound key: `(moduleId, nodeOpId, validatorIndex)`; the `keyIndex` is excluded from the sort key, so the same validator cannot appear twice with different key indices. ::: ```solidity uint256 public constant DATA_FORMAT_LIST_WITH_KEY_INDEX = 2; ``` ### EXIT_TYPE() The exit type code passed to the [TriggerableWithdrawalsGateway](/contracts/triggerable-withdrawals-gateway) when exits are triggered via [`triggerExits`](#triggerexits). ```solidity uint256 public constant EXIT_TYPE = 2; ``` ### SECONDS_PER_SLOT() See [https://ethereum.org/en/developers/docs/blocks/#block-time](https://ethereum.org/en/developers/docs/blocks/#block-time) :::note always returns 12 seconds due to [the Merge](https://ethereum.org/en/roadmap/merge/) ::: ```solidity uint256 public immutable SECONDS_PER_SLOT; ``` ### GENESIS_TIME() See [https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21](https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21) :::note always returns 1606824023 (December 1, 2020, 12:00:23pm UTC) on [Mainnet](https://blog.ethereum.org/2020/11/27/eth2-quick-update-no-21) ::: ```solidity uint256 public immutable GENESIS_TIME; ``` ### PAUSE_INFINITELY() Special value for the infinite pause. See [`pauseFor`](#pausefor) and [`pauseUntil`](#pauseuntil). ```solidity uint256 public constant PAUSE_INFINITELY = type(uint256).max; ``` ## ProcessingState ```solidity struct ProcessingState { uint256 currentFrameRefSlot; uint256 processingDeadlineTime; bytes32 dataHash; bool dataSubmitted; uint256 dataFormat; uint256 requestsCount; uint256 requestsSubmitted; } ``` - `currentFrameRefSlot` โ€” Reference slot for the current reporting frame. - `processingDeadlineTime` โ€” The last time at which a report data can be submitted for the current reporting frame. - `dataHash` โ€” Hash of the report data. Zero bytes if consensus on the hash hasn't been reached yet for the current reporting frame. - `dataSubmitted` โ€” Whether any report data for the current reporting frame has been already submitted. - `dataFormat` โ€” Format of the report data for the current reporting frame. - `requestsCount` โ€” Total number of validator exit requests for the current reporting frame. - `requestsSubmitted` โ€” How many validator exit requests are already submitted for the current reporting frame. ## View methods ### getTotalRequestsProcessed() Returns the total number of validator exit requests ever processed across all received reports. ```solidity function getTotalRequestsProcessed() external view returns (uint256); ``` ### getProcessingState() Returns data processing state for the current reporting frame. See the docs for the [ProcessingState](#processingstate) struct. ```solidity function getProcessingState() external view returns (ProcessingState memory result); ``` ### getConsensusContract() Returns the address of the [HashConsensus](/contracts/hash-consensus) contract instance used by `ValidatorsExitBusOracle`. ```solidity function getConsensusContract() external view returns (address); ``` ### getConsensusReport() Returns the last consensus report hash and metadata. ```solidity function getConsensusReport() external view returns ( bytes32 hash, uint256 refSlot, uint256 processingDeadlineTime, bool processingStarted ); ``` #### Returns | Name | Type | Description | | ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `hash` | `bytes32` | The last reported hash | | `refSlot` | `uint256` | The frame's reference slot: if the data the consensus is being reached upon includes or depends on any onchain state, this state should be queried at the reference slot. If the slot contains a block, the state should include all changes from that block. | | `processingDeadlineTime` | `uint256` | Timestamp of the last slot at which a report can be reported and processed | | `processingStarted` | `bool` | Has the processing of the report been started or not | ### getConsensusVersion() Returns the current consensus version expected by the oracle contract. :::note Consensus version must change every time consensus rules change, meaning that an oracle looking at the same reference slot would calculate a different hash. ::: ```solidity function getConsensusVersion() external view returns (uint256); ``` ### getContractVersion() Returns the current contract version. ```solidity function getContractVersion() public view returns (uint256); ``` ### getLastProcessingRefSlot() Returns the last reference slot for which processing of the report was started. ```solidity function getLastProcessingRefSlot() external view returns (uint256); ``` ### getResumeSinceTimestamp() Returns one of the `timestamp` values: - `PAUSE_INFINITELY` if paused permanently (i.e., with no expiration timestamp) - a first second when get contract get resumed if paused for specific duration (if `timestamp โ‰ฅ block.timestamp`) - some timestamp in past if not paused (if `timestamp < block.timestamp`) ```solidity function getResumeSinceTimestamp() external view returns (uint256 timestamp); ``` ### isPaused() Returns whether the contract is paused or not at the moment. ```solidity function isPaused() public view returns (bool); ``` ### getMaxValidatorsPerReport() Returns the maximum number of validator exit requests that can be processed in a single [`submitExitRequestsData`](#submitexitrequestsdata) payload. ```solidity function getMaxValidatorsPerReport() external view returns (uint256); ``` #### Returns | Name | Type | Description | | ------------------------ | --------- | ---------------------------------------------------- | | `maxValidatorsPerReport` | `uint256` | The maximum number of exit requests allowed per report. | ### getExitRequestLimitFullInfo() Returns information about the current ETH-denominated exit request limit. See [`setExitRequestLimit`](#setexitrequestlimit) for the limit semantics. ```solidity function getExitRequestLimitFullInfo() external view returns ( uint256 maxExitBalanceEth, uint256 balancePerFrameEth, uint256 frameDurationInSec, uint256 prevExitBalanceEth, uint256 currentExitBalanceEth ); ``` #### Returns | Name | Type | Description | | ----------------------- | --------- | ---------------------------------------------------------------------------------------------- | | `maxExitBalanceEth` | `uint256` | Maximum exit balance limit in ETH. | | `balancePerFrameEth` | `uint256` | The exit balance in ETH that can be restored per frame. | | `frameDurationInSec` | `uint256` | The duration of each frame, in seconds, after which `balancePerFrameEth` can be restored. | | `prevExitBalanceEth` | `uint256` | Balance limit in ETH left after previous requests. | | `currentExitBalanceEth` | `uint256` | Current exit balance limit in ETH. Equals `type(uint256).max` if the limit is not set. | ### getDeliveryTimestamp() Returns the timestamp at which the exit requests data corresponding to the given hash was delivered. ```solidity function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 deliveryDateTimestamp); ``` #### Parameters | Name | Type | Description | | ------------------ | --------- | ------------------------------------------------------------------ | | `exitRequestsHash` | `bytes32` | `keccak256(abi.encode(data, dataFormat))` hash of the exit requests data. | #### Reverts - Reverts with `ExitHashNotSubmitted()` if the hash was not submitted. - Reverts with `RequestsNotDelivered()` if the corresponding data was not delivered yet. ### unpackExitRequest() Returns validator exit request data by index. ```solidity function unpackExitRequest( bytes calldata exitRequests, uint256 dataFormat, uint256 index ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); ``` #### Parameters | Name | Type | Description | | -------------- | --------- | ------------------------------------------------------------------------------------------------- | | `exitRequests` | `bytes` | Encoded list of validator exit requests. | | `dataFormat` | `uint256` | Format of the encoded exit requests data: `DATA_FORMAT_LIST=1` or `DATA_FORMAT_LIST_WITH_KEY_INDEX=2`. | | `index` | `uint256` | Index of the exit request within the `exitRequests` list. | #### Returns | Name | Type | Description | | ---------- | --------- | ----------------------------- | | `pubkey` | `bytes` | Public key of the validator. | | `nodeOpId` | `uint256` | ID of the node operator. | | `moduleId` | `uint256` | ID of the staking module. | | `valIndex` | `uint256` | Index of the validator. | #### Reverts - Reverts with `UnsupportedRequestsDataFormat(format)` if the provided data format is not supported. - Reverts with `InvalidRequestsDataLength()` if the provided data is empty or packed incorrectly. - Reverts with `ExitDataIndexOutOfRange(exitDataIndex, requestsCount)` if `index` is out of range. ### MAX_EFFECTIVE_BALANCE_WEIGHT_WC_TYPE_01() Returns the per-validator weight in ETH (32 ETH) used to compute the upper-bound total effective balance for validators of modules with `0x01`-type withdrawal credentials. The value is read from the [OracleReportSanityChecker](/contracts/oracle-report-sanity-checker) contract. ```solidity function MAX_EFFECTIVE_BALANCE_WEIGHT_WC_TYPE_01() public view returns (uint16); ``` ### MAX_EFFECTIVE_BALANCE_WEIGHT_WC_TYPE_02() Returns the per-validator weight in ETH (2048 ETH) used to compute the upper-bound total effective balance for validators of modules with `0x02`-type withdrawal credentials. The value is read from the [OracleReportSanityChecker](/contracts/oracle-report-sanity-checker) contract. ```solidity function MAX_EFFECTIVE_BALANCE_WEIGHT_WC_TYPE_02() public view returns (uint16); ``` ## Methods ### submitReportData() Submits report data for processing. Processing the report emits a [`ValidatorExitRequest`](#validatorexitrequest) event for each request, stores the `keccak256(abi.encode(data.data, data.dataFormat))` hash as delivered (making the data usable with [`triggerExits`](#triggerexits)), and emits the [`RequestsHashSubmitted`](#requestshashsubmitted) and [`ExitDataProcessing`](#exitdataprocessing) events. ```solidity function submitReportData(ReportData calldata data, uint256 contractVersion) external whenResumed; ``` #### Parameters | Name | Type | Description | | ----------------- | ------------ | -------------------------------------------------------------- | | `data` | `ReportData` | The report data. See [`ReportData`](#report-data) for details. | | `contractVersion` | `uint256` | Expected version of the oracle contract. | #### Reverts - Reverts with `ResumedExpected()` if the contract is paused. - Reverts with `SenderNotAllowed()` if the caller doesn't have a `SUBMIT_DATA_ROLE` role and is not a member of the oracle committee. - Reverts with `UnexpectedContractVersion(expectedVersion, version)` if the provided contract version differs from the current one. - Reverts with `UnexpectedConsensusVersion(expectedConsensusVersion, consensusVersion)` if the provided consensus version differs from the expected one. - Reverts with `UnexpectedRefSlot(report.refSlot, refSlot)` if the provided reference slot differs from the current consensus frame's one. - Reverts with `UnexpectedDataHash(report.hash, hash)` if a `keccak256` hash of the ABI-encoded data differs from the last hash. - Reverts with `NoConsensusReportToProcess()` if the report hash data is `0`. - Reverts with `ProcessingDeadlineMissed(deadline)` if the processing deadline for the current consensus frame is missed. - Reverts with `RefSlotAlreadyProcessing()` if the report reference slot is equal to the previous processing reference slot. - Reverts with `UnsupportedRequestsDataFormat(format)` if the provided data format is not `DATA_FORMAT_LIST_WITH_KEY_INDEX` - Reverts with `InvalidRequestsDataLength()` if the provided data is packed incorrectly - Reverts with `UnexpectedRequestsDataLength()` if the number of packed requests is not equal `data.requestsCount` - Reverts if the upper-bound total effective balance of the validators in the report exceeds the limit enforced by `OracleReportSanityChecker.checkExitBusOracleReport` - Reverts with `InvalidModuleId()` if `moduleId` in the provided data is `0` - Reverts with `InvalidRequestsDataSortOrder()` when the provided data is not sorted in the ascending order by `(moduleId, nodeOpId, validatorIndex)` or contains duplicates - Reverts with `InvalidPublicKey(index)` if a provided public key does not match the signing key registered in the staking module at the given `keyIndex` - Reverts with `InvalidRetrievedKeyLength()` if the key retrieved from the staking module has an invalid length ### submitExitRequestsHash() Submits a hash pre-commit for the exit requests data to be delivered later via [`submitExitRequestsData`](#submitexitrequestsdata). This enables a two-step delivery for trusted entities (e.g., Easy Track): first the hash, then the data. ```solidity function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE); ``` #### Parameters | Name | Type | Description | | ------------------ | --------- | ---------------------------------------------------------------------------------------------------------------- | | `exitRequestsHash` | `bytes32` | `keccak256(abi.encode(data, dataFormat))` hash of the exit requests payload to be submitted later via `submitExitRequestsData`. | #### Reverts - Reverts with `ResumedExpected()` if the contract is paused - Reverts with `AccessControl:...` reason if the sender has no `SUBMIT_REPORT_HASH_ROLE` - Reverts with `ExitHashAlreadySubmitted()` if the hash has already been submitted ### submitExitRequestsData() Submits the exit requests payload pre-committed earlier via [`submitExitRequestsHash`](#submitexitrequestshash). Verifies the hash, validates the data, applies the per-report cap and the ETH-denominated exit request limit, and emits a [`ValidatorExitRequest`](#validatorexitrequest) event for each request. Each request debits the exit request limit by the upper-bound effective balance of the validator: 32 ETH for validators of modules with `0x01`-type withdrawal credentials and 2048 ETH for validators of modules with `0x02`-type withdrawal credentials. Structure: ```solidity struct ExitRequestsData { bytes data; uint256 dataFormat; } ``` ```solidity function submitExitRequestsData(ExitRequestsData calldata request) external whenResumed; ``` #### Parameters | Name | Type | Description | | ------------ | --------- | ----------------------------------------------------------------------------------------------------- | | `data` | `bytes` | Tightly packed list of exit requests. | | `dataFormat` | `uint256` | Data format: `DATA_FORMAT_LIST=1` or `DATA_FORMAT_LIST_WITH_KEY_INDEX=2`. | #### Reverts - Reverts with `ResumedExpected()` if the contract is paused - Reverts with `ExitHashNotSubmitted()` if the hash of the provided data was not submitted earlier - Reverts with `RequestsAlreadyDelivered()` if the provided data has already been delivered - Reverts with `UnsupportedRequestsDataFormat(format)` if the provided data format is not supported - Reverts with `InvalidRequestsDataLength()` if the provided data is empty or packed incorrectly - Reverts with `UnexpectedContractVersion(expectedVersion, version)` if the contract version differs from the one at the time of the hash submission - Reverts with `TooManyExitRequestsInReport(requestsCount, maxRequestsPerReport)` if the number of requests exceeds the [`getMaxValidatorsPerReport`](#getmaxvalidatorsperreport) cap - Reverts with `ExitRequestsLimitExceeded(balanceEth, remainingLimitEth)` if the upper-bound total effective balance of the requested validators exceeds the currently available exit request limit - Reverts with `InvalidModuleId()` if `moduleId` in the provided data is `0` - Reverts with `InvalidRequestsDataSortOrder()` when the provided data is not sorted in the ascending order by `(moduleId, nodeOpId, validatorIndex)` or contains duplicates - For the `DATA_FORMAT_LIST_WITH_KEY_INDEX` format, reverts with `InvalidPublicKey(index)` or `InvalidRetrievedKeyLength()` if a provided public key fails validation against the keys registered in the staking module ### triggerExits() Submits Triggerable Withdrawal Requests to the [TriggerableWithdrawalsGateway](/contracts/triggerable-withdrawals-gateway) for the specified validators whose exit requests were delivered earlier via an oracle report or `submitExitRequestsData`. The attached `msg.value` covers the [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) withdrawal request fees; any excess is refunded to `refundRecipient`. ```solidity function triggerExits( ExitRequestsData calldata exitsData, uint256[] calldata exitDataIndexes, address refundRecipient ) external payable whenResumed preservesEthBalance; ``` #### Parameters | Name | Type | Description | | ----------------- | ------------------ | ----------------------------------------------------------------------------------------------- | | `exitsData` | `ExitRequestsData` | The exit requests data delivered earlier via an oracle report or `submitExitRequestsData`. | | `exitDataIndexes` | `uint256[]` | Strictly increasing list of item indexes in `exitsData.data` to be exited via Trigger Exit. | | `refundRecipient` | `address` | Address to return the excess fee to. If set to zero address, the sender is used. | #### Reverts - Reverts with `ResumedExpected()` if the contract is paused - Reverts with `ZeroArgument("msg.value")` if no fee is attached to the call - Reverts with `ZeroArgument("exitDataIndexes")` if the index array is empty - Reverts with `ExitHashNotSubmitted()` if the hash of the provided data was not submitted earlier - Reverts with `RequestsNotDelivered()` if the provided data was not delivered yet - Reverts with `UnsupportedRequestsDataFormat(format)` if the provided data format is not supported - Reverts with `InvalidRequestsDataLength()` if the provided data is empty or packed incorrectly - Reverts with `ExitDataIndexOutOfRange(exitDataIndex, requestsCount)` if any of the provided indexes is out of range - Reverts with `InvalidExitDataIndexSortOrder()` if `exitDataIndexes` is not a strictly increasing array - Reverts with `InvalidModuleId()` if `moduleId` of a selected request is `0` ### setExitRequestLimit() Sets the ETH-denominated limit applied to exit requests delivered via [`submitExitRequestsData`](#submitexitrequestsdata): the maximum exit balance and the frame during which a portion of the limit is restored. Emits the [`ExitBalanceLimitSet`](#exitbalancelimitset) event. ```solidity function setExitRequestLimit( uint256 maxExitBalanceEth, uint256 balancePerFrameEth, uint256 frameDurationInSec ) external onlyRole(EXIT_REQUEST_LIMIT_MANAGER_ROLE); ``` #### Parameters | Name | Type | Description | | -------------------- | --------- | ------------------------------------------------------------------------------------------ | | `maxExitBalanceEth` | `uint256` | The maximum exit balance limit in ETH. | | `balancePerFrameEth` | `uint256` | The exit balance in ETH that can be restored per frame. | | `frameDurationInSec` | `uint256` | The duration of each frame, in seconds, after which `balancePerFrameEth` can be restored. | ### setMaxValidatorsPerReport() Sets the hard cap for the number of validator exit requests that can be processed in a single [`submitExitRequestsData`](#submitexitrequestsdata) payload. Emits the [`SetMaxValidatorsPerReport`](#setmaxvalidatorsperreport-1) event. ```solidity function setMaxValidatorsPerReport(uint256 maxRequests) external onlyRole(EXIT_REQUEST_LIMIT_MANAGER_ROLE); ``` #### Parameters | Name | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------- | | `maxRequests` | `uint256` | The maximum number of exit requests allowed per report. Must be greater than 0. | ### pauseFor() Pause accepting the reports data and forming new validator exit requests for the provided duration in seconds. ```solidity function pauseFor(uint256 _duration) external; ``` #### Parameters | Name | Type | Description | | ----------- | --------- | -------------------------------------------------------------- | | `_duration` | `uint256` | pause duration, seconds (use `PAUSE_INFINITELY` for unlimited) | #### Reverts - Reverts with `ResumedExpected()` if contract is already paused - Reverts with `AccessControl:...` reason if sender has no `PAUSE_ROLE` - Reverts with `ZeroPauseDuration()` if zero duration is passed ### pauseUntil() Pause accepting the reports data and forming new validator exit requests till the given timestamp (inclusive). ```solidity function pauseUntil(uint256 _pauseUntilInclusive) external; ``` #### Parameters | Name | Type | Description | | ---------------------- | --------- | ------------------------------------------ | | `_pauseUntilInclusive` | `uint256` | the last second to pause until (inclusive) | #### Reverts - Reverts with `PauseUntilMustBeInFuture()` if the provided timestamp is in the past - Reverts with `AccessControl:...` reason if the sender has no `PAUSE_ROLE` - Reverts with `ResumedExpected()` if the contract is already paused ### resume() Resume accepting the reports data and forming new validator exit requests. ```solidity function resume() external; ``` #### Reverts - Reverts with `PausedExpected()` if contract is already resumed (i.e., not paused) - Reverts with `AccessControl:...` reason if the sender has no `RESUME_ROLE` ## Permissions ### SUBMIT_DATA_ROLE() An ACL role granting the permission to submit the data for a committee report. ```solidity bytes32 public constant SUBMIT_DATA_ROLE = keccak256("SUBMIT_DATA_ROLE"); ``` ### SUBMIT_REPORT_HASH_ROLE() An ACL role granting the permission to submit a hash of the exit requests data by calling [`submitExitRequestsHash`](#submitexitrequestshash). ```solidity bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); ``` ### EXIT_REQUEST_LIMIT_MANAGER_ROLE() An ACL role granting the permission to set the exit request limits by calling [`setExitRequestLimit`](#setexitrequestlimit) and the per-report cap by calling [`setMaxValidatorsPerReport`](#setmaxvalidatorsperreport). ```solidity bytes32 public constant EXIT_REQUEST_LIMIT_MANAGER_ROLE = keccak256("EXIT_REQUEST_LIMIT_MANAGER_ROLE"); ``` ### PAUSE_ROLE() An ACL role granting the permission to pause accepting the reports data and forming new validator exit requests. ```solidity bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); ``` ### RESUME_ROLE() An ACL role granting the permission to resume accepting the reports data and forming new validator exit requests. ```solidity bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); ``` ### MANAGE_CONSENSUS_CONTRACT_ROLE() An ACL role granting the permission to set the consensus contract address by calling `setConsensusContract`. ```solidity bytes32 public constant MANAGE_CONSENSUS_CONTRACT_ROLE = keccak256("MANAGE_CONSENSUS_CONTRACT_ROLE"); ``` ### MANAGE_CONSENSUS_VERSION_ROLE() An ACL role granting the permission to set the consensus version by calling `setConsensusVersion`. ```solidity bytes32 public constant MANAGE_CONSENSUS_VERSION_ROLE = keccak256("MANAGE_CONSENSUS_VERSION_ROLE"); ``` ## Events ### ValidatorExitRequest() Emits for each validator requested to exit when exit requests data is processed. ```solidity event ValidatorExitRequest( uint256 indexed stakingModuleId, uint256 indexed nodeOperatorId, uint256 indexed validatorIndex, bytes validatorPubkey, uint256 timestamp ); ``` ### RequestsHashSubmitted() Emits when a hash of the exit requests data is stored, either via [`submitExitRequestsHash`](#submitexitrequestshash) or as part of an oracle report submitted via [`submitReportData`](#submitreportdata). ```solidity event RequestsHashSubmitted(bytes32 exitRequestsHash); ``` ### ExitDataProcessing() Emits when exit requests data is delivered, either via [`submitReportData`](#submitreportdata) or [`submitExitRequestsData`](#submitexitrequestsdata). ```solidity event ExitDataProcessing(bytes32 exitRequestsHash); ``` ### ExitBalanceLimitSet() Emits when the exit request limits are set by the [`setExitRequestLimit`](#setexitrequestlimit) call. ```solidity event ExitBalanceLimitSet(uint256 maxExitBalanceEth, uint256 balancePerFrameEth, uint256 frameDurationInSec); ``` ### SetMaxValidatorsPerReport() Emits when the per-report cap is set by the [`setMaxValidatorsPerReport`](#setmaxvalidatorsperreport) call. ```solidity event SetMaxValidatorsPerReport(uint256 maxValidatorsPerReport); ``` ### WarnDataIncompleteProcessing() Emits on attempt of new data submission having not all of the items processed yet. ```solidity event WarnDataIncompleteProcessing( uint256 indexed refSlot, uint256 requestsProcessed, uint256 requestsCount ); ``` ### ConsensusHashContractSet() Emits when the consensus contract address is changed. ```solidity event ConsensusHashContractSet(address indexed addr, address indexed prevAddr); ``` ### ConsensusVersionSet() Emits when a consensus version value is changed. ```solidity event ConsensusVersionSet(uint256 indexed version, uint256 indexed prevVersion); ``` ### ReportSubmitted() Emits when a new consensus report hash is submitted. ```solidity event ReportSubmitted(uint256 indexed refSlot, bytes32 hash, uint256 processingDeadlineTime); ``` ### ReportDiscarded() Emits when consensus report is discarded. ```solidity event ReportDiscarded(uint256 indexed refSlot, bytes32 hash); ``` ### ProcessingStarted() Emits when report data processing is started. ```solidity event ProcessingStarted(uint256 indexed refSlot, bytes32 hash); ``` ### WarnProcessingMissed() Emits on `submitConsensusReport` when `refSlot != prevSubmittedRefSlot && prevProcessingRefSlot != prevSubmittedRefSlot` ```solidity event WarnProcessingMissed(uint256 indexed refSlot); ``` ### Paused() Emits when the contract is paused either by the `pauseFor` or `pauseUntil` calls. ```solidity event Paused(uint256 duration); ``` ### Resumed() Emits when the contract is resumed by the `resume` call. ```solidity event Resumed(); ``` --- # VaultHub - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/vaults/VaultHub.sol) - [Deployed contract](https://etherscan.io/address/0x1d201BE093d847f6446530Efb0E8Fb426d176709) Central registry and lifecycle manager for StakingVaults connected to the Lido protocol. Handles vault connection, minting/burning stETH against vault collateral, rebalancing, fee settlement, and bad debt management. ## What is VaultHub? VaultHub is the coordinator between individual StakingVaults and the Lido protocol: - **Connection management**: vaults connect permissionlessly (with OperatorGrid limits) and can disconnect voluntarily or be disconnected by governance - **Minting/burning**: vault owners mint stETH shares against their vault's total value as collateral - **Rebalancing**: when vaults become unhealthy, anyone can trigger forced rebalancing using available vault funds - **Fee settlement**: Lido fees accrue on vaults and can be settled permissionlessly - **Bad debt handling**: governance can socialize bad debt to other vaults or internalize it as protocol loss - **Connect deposit lock**: VaultHub enforces the 1 ETH connect deposit as minimal reserve - **Beacon chain deposits auto-pause**: deposits are automatically paused when vaults have outstanding obligations ## Inherits - [PausableUntilWithRoles](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.25/utils/PausableUntilWithRoles.sol) ## Constants | Constant | Value | Description | | -------------------------- | ------------------ | -------------------------------------------------------------------- | | `CONNECT_DEPOSIT` | 1 ether | ETH locked on connection, returned on disconnect | | `REPORT_FRESHNESS_DELTA` | 2 days | Maximum age for a report to be considered fresh | | `MIN_BEACON_DEPOSIT` | 1 ether | Threshold for beacon chain deposits pause on unsettled fees | | `PDG_ACTIVATION_DEPOSIT` | 31 ether | ETH required per validator activation after PDG predeposit | | `DISCONNECT_NOT_INITIATED` | `type(uint48).max` | Special value indicating vault is connected (not pending disconnect) | | `TOTAL_BASIS_POINTS` | 10000 | Basis points denominator | ## Immutable variables | Variable | Description | | ----------------------------- | ------------------------------------------------------- | | `LIDO` | stETH contract for minting/burning external shares | | `LIDO_LOCATOR` | Protocol locator for resolving contract addresses | | `CONSENSUS_CONTRACT` | HashConsensus contract for ref slot tracking | | `MAX_RELATIVE_SHARE_LIMIT_BP` | Maximum share limit relative to Lido TVL (basis points) | ## Roles | Role | Description | | ------------------------ | ---------------------------------------------------------- | | `VAULT_MASTER_ROLE` | Can force disconnect vaults from the hub | | `REDEMPTION_MASTER_ROLE` | Can set liability shares targets for Lido Core redemptions | | `VALIDATOR_EXIT_ROLE` | Can trigger forced validator exits for unhealthy vaults | | `BAD_DEBT_MASTER_ROLE` | Can socialize or internalize bad debt from vaults | ## Storage ### ERC-7201 Namespaced Storage ```solidity struct Storage { mapping(address vault => VaultRecord) records; mapping(address vault => VaultConnection) connections; address[] vaults; // 1-based array, index 0 reserved RefSlotCache.Uint104WithCache badDebtToInternalize; } ``` ## Structs ### VaultConnection Connection parameters for a vault: ```solidity struct VaultConnection { address owner; // Vault owner address uint96 shareLimit; // Maximum stETH shares mintable uint96 vaultIndex; // Index in vaults array (1-based, 0 = not connected) uint48 disconnectInitiatedTs; // Timestamp when disconnect started (max = connected) uint16 reserveRatioBP; // Reserve ratio (e.g., 30% = 3000) uint16 forcedRebalanceThresholdBP; // Health threshold for forced rebalance uint16 infraFeeBP; // Infrastructure fee (basis points) uint16 liquidityFeeBP; // Liquidity fee (basis points) uint16 reservationFeeBP; // Reservation fee (basis points) bool beaconChainDepositsPauseIntent; // Owner's intent to pause beacon deposits } ``` ### VaultRecord Accounting record for a vault: ```solidity struct VaultRecord { Report report; // Latest oracle report uint96 maxLiabilityShares; // Peak liability shares in current period uint96 liabilityShares; // Current liability shares DoubleRefSlotCache.Int104WithCache[2] inOutDelta; // Cumulative deposits - withdrawals uint128 minimalReserve; // min(CONNECT_DEPOSIT, slashingReserve) uint128 redemptionShares; // Shares marked for Lido Core redemption uint128 cumulativeLidoFees; // Total accrued Lido fees uint128 settledLidoFees; // Total settled Lido fees } ``` ### Report Oracle report snapshot: ```solidity struct Report { uint104 totalValue; // Total vault value (ETH) int104 inOutDelta; // inOutDelta at report time uint48 timestamp; // Report timestamp } ``` ## Obligations mechanism Vaults have obligations that must be covered before withdrawals: 1. **Health restoration**: Shares to burn/rebalance to restore health ratio 2. **Redemptions**: Shares marked as `redemptionShares` for Lido Core 3. **Fee settlement**: Accrued but unsettled Lido fees (โ‰ฅ1 ETH triggers deposit pause) The `obligations()` view returns `(sharesToBurn, feesToSettle)`. If sharesToBurn is `type(uint256).max`, the vault has bad debt. ### Beacon chain deposits auto-pause Deposits are automatically paused when: - Vault is unhealthy (health shortfall > 0) - Vault has redemption shares to cover - Unsettled Lido fees โ‰ฅ `MIN_BEACON_DEPOSIT` (1 ETH) Once obligations are cleared and owner hasn't set manual pause intent, deposits resume automatically. ### Manual pause intent Vault owners can explicitly pause or resume beacon chain deposits via `pauseBeaconChainDeposits()` and `resumeBeaconChainDeposits()` (typically through Dashboard). This toggles `beaconChainDepositsPauseIntent`: - If pause intent is set, deposits stay paused regardless of obligations. - If pause intent is cleared, deposits still remain paused until obligations are covered. ## View methods ### vaultsCount() ```solidity function vaultsCount() external view returns (uint256) ``` Returns the number of vaults connected to the hub. ### vaultByIndex(uint256 \_index) ```solidity function vaultByIndex(uint256 _index) external view returns (address) ``` Returns vault address by 1-based index. Indexes are not stable across transactions. ### vaultConnection(address \_vault) ```solidity function vaultConnection(address _vault) external view returns (VaultConnection memory) ``` Returns connection parameters for a vault. Returns empty struct if not connected. ### vaultRecord(address \_vault) ```solidity function vaultRecord(address _vault) external view returns (VaultRecord memory) ``` Returns accounting record for a vault. Returns empty struct if not connected. ### isVaultConnected(address \_vault) ```solidity function isVaultConnected(address _vault) external view returns (bool) ``` Returns true if vault is connected (or pending disconnect). ### isPendingDisconnect(address \_vault) ```solidity function isPendingDisconnect(address _vault) external view returns (bool) ``` Returns true if vault disconnect has been initiated and awaiting completion. ### totalValue(address \_vault) ```solidity function totalValue(address _vault) external view returns (uint256) ``` Returns current total value of the vault (report value + inOutDelta changes). ### liabilityShares(address \_vault) ```solidity function liabilityShares(address _vault) external view returns (uint256) ``` Returns current liability shares (minted stETH) of the vault. ### locked(address \_vault) ```solidity function locked(address _vault) external view returns (uint256) ``` Returns amount of ETH locked on the vault based on current liability and reserve ratio. ### maxLockableValue(address \_vault) ```solidity function maxLockableValue(address _vault) external view returns (uint256) ``` Returns maximum ETH that can be locked given current total value. ### totalMintingCapacityShares(address \_vault, int256 \_deltaValue) ```solidity function totalMintingCapacityShares(address _vault, int256 _deltaValue) external view returns (uint256) ``` Returns total shares that can be minted, accounting for reserve ratio, minimal reserve, and operator grid limits. `_deltaValue` allows simulating value changes. ### withdrawableValue(address \_vault) ```solidity function withdrawableValue(address _vault) external view returns (uint256) ``` Returns ETH instantly withdrawable from the vault (accounts for locked value, redemptions, and unsettled fees). ### latestReport(address \_vault) ```solidity function latestReport(address _vault) external view returns (Report memory) ``` Returns the latest oracle report for the vault. ### isReportFresh(address \_vault) ```solidity function isReportFresh(address _vault) external view returns (bool) ``` Returns true if the vault's report is considered fresh (within `REPORT_FRESHNESS_DELTA` of latest oracle report). ### isVaultHealthy(address \_vault) ```solidity function isVaultHealthy(address _vault) external view returns (bool) ``` Returns true if vault's total value meets the forced rebalance threshold. ### healthShortfallShares(address \_vault) ```solidity function healthShortfallShares(address _vault) external view returns (uint256) ``` Returns shares needed to restore vault health. Returns `type(uint256).max` if bad debt (impossible to fix via rebalance). ### obligationsShortfallValue(address \_vault) ```solidity function obligationsShortfallValue(address _vault) external view returns (uint256) ``` Returns ETH shortfall needed to cover all obligations. ### obligations(address \_vault) ```solidity function obligations(address _vault) external view returns (uint256 sharesToBurn, uint256 feesToSettle) ``` Returns the vault's current obligations: shares to burn/rebalance and fees to settle. ### settleableLidoFeesValue(address \_vault) ```solidity function settleableLidoFeesValue(address _vault) external view returns (uint256) ``` Returns Lido fees that can currently be settled (limited by available withdrawable funds). ### badDebtToInternalize() ```solidity function badDebtToInternalize() external view returns (uint256) ``` Returns bad debt shares pending internalization as protocol loss. ### badDebtToInternalizeForLastRefSlot() ```solidity function badDebtToInternalizeForLastRefSlot() external view returns (uint256) ``` Returns bad debt shares that were pending at the last reference slot (for oracle accounting). ## Methods ### initialize(address \_admin) ```solidity function initialize(address _admin) external initializer ``` Initializes the VaultHub with admin address. ### connectVault(address \_vault) ```solidity function connectVault(address _vault) external whenResumed ``` Connects a vault to the hub permissionlessly. Vault must: - Be deployed by a valid factory - Have `msg.sender` as current owner - Have `VaultHub` as pending owner - Not be ossified - Have PDG as depositor - Have staged balance matching pending activations ร— 31 ETH - Have at least `CONNECT_DEPOSIT` (1 ETH) available balance Connection parameters are fetched from OperatorGrid based on the vault's tier. ### voluntaryDisconnect(address \_vault) ```solidity function voluntaryDisconnect(address _vault) external whenResumed ``` Initiates voluntary disconnect. Requires: - `msg.sender` is vault owner - Fresh report - Zero liability shares - Full fee settlement (if funds available) ### disconnect(address \_vault) ```solidity function disconnect(address _vault) external onlyRole(VAULT_MASTER_ROLE) ``` Governance-initiated disconnect. Same requirements as voluntary disconnect but allows partial fee settlement. ### updateConnection(...) ```solidity function updateConnection( address _vault, uint256 _shareLimit, uint256 _reserveRatioBP, uint256 _forcedRebalanceThresholdBP, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) external ``` Updates vault connection parameters. Only callable by OperatorGrid. Requires fresh report and validates new parameters don't breach minting capacity. ### fund(address \_vault) ```solidity function fund(address _vault) external payable whenResumed ``` Funds the vault with ETH. Only callable by vault owner. ### withdraw(address \_vault, address \_recipient, uint256 \_ether) ```solidity function withdraw(address _vault, address _recipient, uint256 _ether) external whenResumed ``` Withdraws ETH from vault. Only callable by vault owner. Requires fresh report and respects withdrawable limits. ### mintShares(address \_vault, address \_recipient, uint256 \_amountOfShares) ```solidity function mintShares(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed ``` Mints stETH shares backed by vault collateral. Only callable by vault owner. Requires fresh report. ### burnShares(address \_vault, uint256 \_amountOfShares) ```solidity function burnShares(address _vault, uint256 _amountOfShares) public whenResumed ``` Burns stETH shares from VaultHub balance to reduce liability. Only callable by vault owner. ### transferAndBurnShares(address \_vault, uint256 \_amountOfShares) ```solidity function transferAndBurnShares(address _vault, uint256 _amountOfShares) external ``` Transfers shares from `msg.sender` to VaultHub and burns them. For EOA vault owners. ### rebalance(address \_vault, uint256 \_shares) ```solidity function rebalance(address _vault, uint256 _shares) external whenResumed ``` Voluntary rebalance by vault owner. Withdraws ETH and burns corresponding shares. ### forceRebalance(address \_vault) ```solidity function forceRebalance(address _vault) external ``` Permissionless forced rebalance for unhealthy vaults. Uses all available balance to cover obligations. ### settleLidoFees(address \_vault) ```solidity function settleLidoFees(address _vault) external ``` Permissionless fee settlement. Sends unsettled fees to treasury. ### setLiabilitySharesTarget(address \_vault, uint256 \_liabilitySharesTarget) ```solidity function setLiabilitySharesTarget(address _vault, uint256 _liabilitySharesTarget) external onlyRole(REDEMPTION_MASTER_ROLE) ``` Sets target liability, marking excess as redemption shares. Used for Lido Core redemptions. ### transferVaultOwnership(address \_vault, address \_newOwner) ```solidity function transferVaultOwnership(address _vault, address _newOwner) external ``` Transfers vault ownership within VaultHub without disconnecting. ### pauseBeaconChainDeposits(address \_vault) ```solidity function pauseBeaconChainDeposits(address _vault) external ``` Owner sets intent to pause beacon chain deposits. ### resumeBeaconChainDeposits(address \_vault) ```solidity function resumeBeaconChainDeposits(address _vault) external ``` Owner clears pause intent. Deposits may remain paused if obligations exist. ### requestValidatorExit(address \_vault, bytes calldata \_pubkeys) ```solidity function requestValidatorExit(address _vault, bytes calldata _pubkeys) external ``` Emits exit request events for node operator. Only callable by vault owner. ### triggerValidatorWithdrawals(...) ```solidity function triggerValidatorWithdrawals( address _vault, bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient ) external payable ``` Triggers EIP-7002 validator withdrawals. Partial withdrawals require fresh report and sufficient amount to cover obligations shortfall. ### forceValidatorExit(address \_vault, bytes calldata \_pubkeys, address \_refundRecipient) ```solidity function forceValidatorExit( address _vault, bytes calldata _pubkeys, address _refundRecipient ) external payable onlyRole(VALIDATOR_EXIT_ROLE) ``` Forces full validator exits for vaults with obligations shortfall. ### socializeBadDebt(address \_badDebtVault, address \_vaultAcceptor, uint256 \_maxSharesToSocialize) ```solidity function socializeBadDebt( address _badDebtVault, address _vaultAcceptor, uint256 _maxSharesToSocialize ) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256) ``` Transfers bad debt to another vault of the same node operator. Requires fresh reports for both vaults. ### internalizeBadDebt(address \_badDebtVault, uint256 \_maxSharesToInternalize) ```solidity function internalizeBadDebt( address _badDebtVault, uint256 _maxSharesToInternalize ) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256) ``` Internalizes bad debt as protocol loss. Requires fresh report. ### decreaseInternalizedBadDebt(uint256 \_amountOfShares) ```solidity function decreaseInternalizedBadDebt(uint256 _amountOfShares) external ``` Called by Accounting contract to clear internalized bad debt after settlement. ### applyVaultReport(...) ```solidity function applyVaultReport( address _vault, uint256 _reportTimestamp, uint256 _reportTotalValue, int256 _reportInOutDelta, uint256 _reportCumulativeLidoFees, uint256 _reportLiabilityShares, uint256 _reportMaxLiabilityShares, uint256 _reportSlashingReserve ) external whenResumed ``` Applies oracle report to vault. Only callable by LazyOracle. Updates vault record and may complete pending disconnect. ### proveUnknownValidatorToPDG(address \_vault, IPredepositGuarantee.ValidatorWitness calldata \_witness) ```solidity function proveUnknownValidatorToPDG( address _vault, IPredepositGuarantee.ValidatorWitness calldata _witness ) external ``` Proves unknown validators to PDG. Only callable by vault owner. ### collectERC20FromVault(address \_vault, address \_token, address \_recipient, uint256 \_amount) ```solidity function collectERC20FromVault( address _vault, address _token, address _recipient, uint256 _amount ) external ``` Recovers ERC-20 tokens from vault. Only callable by vault owner. ## Events ```solidity event VaultConnected( address indexed vault, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ); event VaultConnectionUpdated( address indexed vault, address indexed nodeOperator, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP ); event VaultFeesUpdated( address indexed vault, uint256 preInfraFeeBP, uint256 preLiquidityFeeBP, uint256 preReservationFeeBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ); event VaultDisconnectInitiated(address indexed vault); event VaultDisconnectCompleted(address indexed vault); event VaultDisconnectAborted(address indexed vault, uint256 slashingReserve); event VaultReportApplied( address indexed vault, uint256 reportTimestamp, uint256 reportTotalValue, int256 reportInOutDelta, uint256 reportCumulativeLidoFees, uint256 reportLiabilityShares, uint256 reportMaxLiabilityShares, uint256 reportSlashingReserve ); event MintedSharesOnVault(address indexed vault, uint256 amountOfShares, uint256 lockedAmount); event BurnedSharesOnVault(address indexed vault, uint256 amountOfShares); event VaultRebalanced(address indexed vault, uint256 sharesBurned, uint256 etherWithdrawn); event VaultInOutDeltaUpdated(address indexed vault, int256 inOutDelta); event ForcedValidatorExitTriggered(address indexed vault, bytes pubkeys, address refundRecipient); event VaultOwnershipTransferred(address indexed vault, address indexed newOwner, address indexed oldOwner); event LidoFeesSettled(address indexed vault, uint256 transferred, uint256 cumulativeLidoFees, uint256 settledLidoFees); event VaultRedemptionSharesUpdated(address indexed vault, uint256 redemptionShares); event BeaconChainDepositsPauseIntentSet(address indexed vault, bool pauseIntent); event BadDebtSocialized(address indexed vaultDonor, address indexed vaultAcceptor, uint256 badDebtShares); event BadDebtWrittenOffToBeInternalized(address indexed vault, uint256 badDebtShares); ``` ## Related - [StakingVault](/contracts/staking-vault) - [LazyOracle](/contracts/lazy-oracle) - [OperatorGrid](/contracts/operator-grid) - [Dashboard](/contracts/dashboard) - [PredepositGuarantee](/contracts/predeposit-guarantee) --- # WithdrawalQueueERC721 - [Source code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/WithdrawalQueueERC721.sol) - [Deployed contract](https://etherscan.io/address/0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1) A FIFO queue for `stETH` withdrawal requests and an `unstETH` NFT implementation representing the position in the queue. Access to lever methods is restricted using the functionality of the [AccessControlEnumerable](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/utils/access/AccessControlEnumerable.sol) contract and a bunch of [granular roles](#roles). ## What is WithdrawalQueueERC721? This contract is a main entry point to exchange `stETH` for underlying ether directly via Lido protocol. It is responsible for: - managing a queue of withdrawal requests - committing withdrawal request finalization as a part of the [AccountingOracle](/contracts/accounting-oracle) report - storing `stETH` before and ether after the finalization - transfer reserved ether to the user upon the claim Also, the contract is [ERC-721](https://eips.ethereum.org/EIPS/eip-721) `unstETH` NFT with metadata extension representing the right to claim underlying ether once the request is finalized. This NFT is minted upon request and burned on the claim. [ERC-4906](https://eips.ethereum.org/EIPS/eip-4906) is used to update the metadata as soon as the finalization status of the request is changed. ## Request To request a withdrawal, one needs to approve the amount of `stETH` or `wstETH` to this contract or sign the [ERC-2612 Permit](https://eips.ethereum.org/EIPS/eip-2612), and then call the appropriate `requestWithdrawals*` method. The **minimal** amount for a request is `100 wei`, and the **maximum** is `1000 eth`. More significant amounts should be split into several requests, which allows us to avoid clogging the queue with an extra large request. During this call, the request is placed in the queue, and the related `unstETH` NFT is minted. The following structure represents the request: ```sol struct WithdrawalRequestStatus { uint256 amountOfStETH; uint256 amountOfShares; address owner; uint256 timestamp; bool isFinalized; bool isClaimed; } ``` where - **`amountOfStETH`** โ€” the number of `stETH` tokens transferred to the contract upon request - **`amountOfShares`** โ€” the number of underlying shares corresponding to transferred `stETH` tokens. See [Lido rebasing chapter](lido.md#rebase) to learn about the shares mechanic - **`owner`** โ€” the owner's address for this request. The owner is also a holder of the `unstETH` NFT and can transfer the ownership and claim the underlying ether once finalized - **`timestamp`** โ€” the creation time of the request - **`isFinalized`** โ€” finalization status of the request; finalized requests are available to claim - **`isClaimed`** โ€” the claim status of the request. Once claimed, NFT is burned, and the request is not available to claim again :::note The amount of ether that will be withdrawn is limited to the number of `stETH` tokens transferred to this contract at the moment of request. So, the user will not receive the rewards for the period of time while their tokens stay in the queue. ::: ## Finalization After filing a withdrawal request, one can only claim it once finalization occurs. [Accounting Oracle](accounting-oracle.md) report finalizes a batch of withdrawal requests, choosing the `_maxShareRate` and the size of the batch taking in account following factors: - If there is enough ether to fulfill the request. Ether can be obtained from the Lido buffer, which is filled from the new users' stake, Beacon chain partial and full withdrawals, protocol tips, and MEV rewards. Withdrawals are prioritized over deposits, so ether can't be deposited to the Beacon chain if some withdrawal requests can be fulfilled. - if enough time has passed since the withdrawal request was placed in the queue (timelock) - If there was some massive loss for the protocol on the Beacon Chain side since the withdrawal request was filed. It can lead to finalization by the rate lower than 1:1 if the loss will be high enough to be not covered with daily rewards (never happened before) :::note To put it simply, token holders don't receive rewards but still take risks during withdrawal. Rewards, acquired since the stETH was locked in the WithdrawalQueue, are burned upon the finalization, effectively distributing them among the other token holders. ::: So, the finalization sets the final value of the request, locks ether on the balance of this contract, and burns the underlying `stETH` and the queue may look like this in arbitrary moment: ```mermaid graph LR subgraph queue direction LR B---D subgraph unfinalized; A[1 stETH]---B[1.1 stETH] end subgraph finalized; D(0.3 ETH)---E(1000 ETH) end end classDef gr fill:#d0f0c0,stroke:#333,stroke-width:3px;; classDef r fill:#fa8072,stroke:#333,stroke-width:3px;; class A,B r; class D,E gr; ``` ## Claim When the request is finalized, it can be claimed by the current owner, transferring the reserved amount of ether to the recipient's address and burning the withdrawal NFT. To see if the request is claimable, one can get its status using `getWithdrawalStatus()` or subscribe to the event `WithdrawalsFinalized(uint256 from, uint256 to, ...)`, which is emitted once the batch of requests with ids in the range `(from, to]` is finalized. ## Standards Contract implements the following Ethereum standards: - [ERC-721: Non-Fungible Token Standard](https://eips.ethereum.org/EIPS/eip-721) - [ERC-165: Standard Interface Detection](https://eips.ethereum.org/EIPS/eip-165) - [ERC-4906: EIP-721 Metadata Update Extension](https://eips.ethereum.org/EIPS/eip-4906) ## `ERC-721`-related Methods ### name() Returns the token collection name. ```sol function name() view returns (string memory) ``` ### symbol() Returns the token collection symbol. ```sol function symbol() view returns (string memory) ``` ### tokenURI() Returns the Uniform Resource Identifier (URI) for the `_requestId` token. Returns an empty string if no base URI and no `NFTDescriptor` address are set. ```sol function tokenURI(uint256 _requestId) view returns (string memory) ``` ### balanceOf() Returns the number of tokens in the `_owner`'s account. ```sol function balanceOf(address _owner) view returns (uint256 balance) ``` :::note Reverts if `_owner` is zero address ::: ### ownerOf() Returns the owner of the `_requestId` token. ```sol function ownerOf(uint256 _requestId) view returns (address owner) ``` :::note Requirements: - `_requestId` request must exist. - `_requestId` request must not be claimed. ::: ### approve() Gives permission to `_to` to transfer the `_requestId` token to another account. The approval is cleared when the token is transferred. Emits an `Approval` event. ```sol function approve(address _to, uint256 _requestId) ``` :::note Requirements: - The caller must own the token or be an approved operator. - `_requestId` must exist. - `_to` must not be the owner ::: ### getApproved() Returns the account approved for the `_requestId` token. ```sol function getApproved(uint256 _requestId) view returns (address) ``` :::note Reverts if no `_requestId` exists ::: ### setApprovalForAll() Approve or remove `_operator` as an operator for the caller. Operators can call `transferFrom` or `safeTransferFrom` for any token owned by the caller. Emits an `ApprovalForAll` event. ```sol function setApprovalForAll(address _operator, bool _approved) ``` :::note Reverts if `msg.sender` is equal to `_operator` ::: ### isApprovedForAll() Returns `true` if the `_operator` is allowed to manage all of the assets of the `_owner`. ```sol function isApprovedForAll(address _owner, address _operator) view returns (bool) ``` ### safeTransferFrom() Safely transfers the `_requestId` token from `_from` to `_to`, checking first that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked. If a version with `_data` parameter is used, it passed to `IERC721Receiver.onERC721Received()` of the target smart contract as an argument. Emits a `Transfer` event. ```sol function safeTransferFrom(address _from, address _to, uint256 _requestId) function safeTransferFrom(address _from, address _to, uint256 _requestId, bytes memory _data) ``` :::note Requirements: - `_from` cannot be the zero address. - `_to` cannot be the zero address. - `_requestId` token must exist and be owned by `_from`. - If the caller is not `_from`, it must have been allowed to move this token by either `approve()` or `setApprovalForAll()`. - If `_to` refers to a smart contract, it must implement `IERC721Receiver` interface ::: ### transferFrom() Transfers the `_requestId` token from `_from` to `_to`. Emits a `Transfer` event. **WARNING**: Usage of this method is discouraged, use `safeTransferFrom()` whenever possible. ```sol function transferFrom(address _from, address _to, uint256 _requestId) ``` :::note Requirements: - `_from` cannot be the zero address. - `_to` cannot be the zero address. - `_requestId` token must be owned by `_from`. - If the caller is not `_from`, it must be approved to move this token by either `approve()` or `setApprovalForAll()`. ::: ### getBaseUri() Returns the base URI for computing token URI. If set, the resulting URI for each token will be the concatenation of the base URI and the `_requestId`. ```sol function getBaseURI() view returns (string memory) ``` ### getNFTDescriptorAddress() Returns the address of the `NFTDescriptor` contract responsible for the token URI generation. ```sol function getNFTDescriptorAddress() view returns (address) ``` ## `ERC-165`-related Methods ### supportsInterface() Returns `true` if this contract implements the interface defined by `interfaceId`. See the [ERC-165](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) to learn more about how these ids are created. ```sol function supportsInterface(bytes4 interfaceId) view returns (bool) ``` :::note This contract returns `true` for `IERC721`, `IERC721Metadata`, `IERC4906`, `IAccessControlEnumerable`, `IAccessControl` and `IERC165` itself. ::: ## Queue-related Methods ### requestWithdrawals() Batch request the `_amounts` of `stETH` for withdrawal to the `_owner` address. For each request, the respective amount of `stETH` is transferred to this contract address, and an `unstETH` NFT is minted to the `_owner` address. ```sol function requestWithdrawals(uint256[] _amounts, address _owner) returns (uint256[] requestIds) ``` Returns the array of ids for each created request. Emits `WithdrawalRequested` and `Transfer` events. :::note Requirements: - withdrawals must not be paused - `stETH` balance of `msg.sender` must be greater than or equal to the sum of all `_amounts` - there must be approval from the `msg.sender` to this contract address for the overall amount of `stETH` token transfer - each amount in `_amounts` must be greater than or equal to `MIN_STETH_WITHDRAWAL_AMOUNT` and lower than or equal to `MAX_STETH_WITHDRAWAL_AMOUNT` ::: ### requestWithdrawalsWstETH() Batch request the `_amounts` of `wstETH` for withdrawal to the `_owner` address. For each request, the respective amount of `wstETH` is transferred to this contract address, unwrapped to `stETH`, and an `unstETH` NFT is minted to the `_owner` address. ```sol function requestWithdrawalsWstETH(uint256[] _amounts, address _owner) returns (uint256[] requestIds) ``` Returns the array of ids for each created request. Emits `WithdrawalRequested` and `Transfer` events. :::note Requirements: - withdrawals must not be paused - `wstETH` balance of `msg.sender` must be greater than or equal to the sum of all `_amounts` - there must be approval from the `msg.sender` to this contract address for the overall amount of `wstETH` token transfer - each amount in `_amounts` must have `getPooledEthByShares(amount)` being greater than `MIN_STETH_WITHDRAWAL_AMOUNT` and lower than `MAX_STETH_WITHDRAWAL_AMOUNT` ::: ### requestWithdrawalsWithPermit() Batch request the `_amounts` of `stETH` for withdrawal to the `_owner` address. For each request, the respective amount of `stETH` is transferred to this contract address, and an `unstETH` NFT is minted to the `_owner` address. `ERC-2612` permit is used to approve the token transfer. ```sol function requestWithdrawalsWithPermit( uint256[] _amounts, address _owner, PermitInput _permit ) returns (uint256[] requestIds) ``` where `_permit` is [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) signed permit structure defined as: ```sol struct PermitInput { uint256 value; uint256 deadline; uint8 v; bytes32 r; bytes32 s; } ``` Returns the array of ids for each created request. Emits `WithdrawalRequested` and `Transfer` events. :::note Requirements: - withdrawals must not be paused - `stETH` balance of `msg.sender` must be greater than or equal to the sum of all `_amounts` - permit must have a valid signature, `value` greater than the sum of all `_amounts`, and the `deadline` not expired - each amount in `_amounts` must be greater than or equal to `MIN_STETH_WITHDRAWAL_AMOUNT` and lower than or equal to `MAX_STETH_WITHDRAWAL_AMOUNT` ::: ### requestWithdrawalsWstETHWithPermit() Batch request the `_amounts` of `wstETH` for withdrawal to the `_owner` address. For each request, the respective amount of `wstETH` is transferred to this contract address, unwrapped to `stETH`, and an `unstETH` NFT is minted to the `_owner` address.`ERC-2612` permit is used to approve the token transfer. ```sol function requestWithdrawalsWstETHWithPermit( uint256[] _amounts, address _owner, PermitInput _permit ) returns (uint256[] requestIds) ``` where `_permit` is [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) signed permit structure defined as: ```sol struct PermitInput { uint256 value; uint256 deadline; uint8 v; bytes32 r; bytes32 s; } ``` Returns the array of ids for each created request. Emits `WithdrawalRequested` and `Transfer` events. :::note Requirements: - withdrawals must not be paused - `wstETH` balance of `msg.sender` must be greater than or equal to the sum of all `_amounts` - permit must have a valid signature, `value` greater than the sum of all `_amounts`, and the `deadline` not expired - each amount in `_amounts` must have `getPooledEthByShares(amount)` being greater than `MIN_STETH_WITHDRAWAL_AMOUNT` and lower than `MAX_STETH_WITHDRAWAL_AMOUNT` ::: ### getWithdrawalRequests() Returns all withdrawal requests that belong to the `_owner` address. ```sol function getWithdrawalRequests(address _owner) view returns (uint256[] requestsIds) ``` :::warning This operation will copy the entire storage to memory, which can be quite expensive. This method is designed to mostly be used by view accessors that are queried without gas fees. Developers should keep in mind that this function has an unbounded cost, and using it as part of a state-changing function may render the function uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. ::: ### getWithdrawalStatus() Returns `statuses` for requests with ids in `_requestIds`. ```sol function getWithdrawalStatus(uint256[] _requestIds) view returns (WithdrawalRequestStatus[] statuses) ``` Returns an array of `WithdrawalRequestStatus` structures, defined as: ```sol struct WithdrawalRequestStatus { uint256 amountOfStETH; uint256 amountOfShares; address owner; uint256 timestamp; bool isFinalized; bool isClaimed; } ``` where - **`amountOfStETH`** โ€” the number of `stETH` tokens transferred to the contract upon request - **`amountOfShares`** โ€” the number of underlying shares corresponding to transferred `stETH` tokens. See [Lido rebasing chapter](lido.md#rebase) to learn about the shares mechanic - **`owner`** โ€” the owner's address for this request. The owner is also a holder of the `unstETH` NFT and can transfer the ownership and claim the underlying ether once finalized - **`timestamp`** โ€” the creation time of the request - **`isFinalized`** โ€” finalization status of the request; finalized requests are available to claim - **`isClaimed`** โ€” the claim status of the request. Once claimed, NFT is burned, and the request is not available to claim again ### getClaimableEther() Returns amounts of ether available for claiming for each provided request id. ```sol function getClaimableEther(uint256[] _requestIds, uint256[] _hints) view returns (uint256[] claimableEthValues) ``` where - **`_requestIds`** โ€” the array of request id to check the claimable ether for - **`_hints`** โ€” checkpoint hint for each request id. Can be obtained by calling [`findCheckpointHints()`](#findcheckpointhints) Returns the array of ether amounts available for claiming for each request id. The amount is equal to 0 if the request is not finalized or already claimed. ### claimWithdrawalsTo() Claim a batch of withdrawal requests if they are finalized, sending ether to `_recipient` address. ```sol function claimWithdrawalsTo(uint256[] _requestIds, uint256[] _hints, address _recipient) ``` where - **`_requestIds`** โ€” the array of request id to check the claimable ether for - **`_hints`** โ€” checkpoint hint for each request id. Can be obtained by calling [`findCheckpointHints()`](#findcheckpointhints) - **`_recipient`** โ€” the address of the recipient for claimed ether Emits a batch of `Transfer` to zero address and `WithdrawalClaimed` events. :::note Requirements: - all `_requestIds` must exist, be finalized and not claimed - all `_hints` must be valid for respective requests - `msg.sender` must be the owner of all the requests - `_recipient` must not be zero ::: ### claimWithdrawals() Claim a batch of withdrawal requests if they are finalized, sending ether to `msg.sender` address. ```sol function claimWithdrawals(uint256[] _requestIds, uint256[] _hints) ``` where - **`_requestIds`** โ€” the array of request id to check the claimable ether for - **`_hints`** โ€” checkpoint hint for each request id. Can be obtained by calling [`findCheckpointHints()`](#findcheckpointhints) Emits a batch of `Transfer` to zero address and `WithdrawalClaimed` events. :::note Requirements: - all `_requestIds` must exist, be finalized and not claimed - all `_hints` must be valid for respective requests - `msg.sender` must be the owner of all the requests ::: ### claimWithdrawal() Claims the `_requestId` withdrawal request, sending ether to `msg.sender` address. ```sol function claimWithdrawal(uint256 _requestId) ``` Emits a `Transfer` to zero address and `WithdrawalClaimed` event. :::note Requirements: - `msg.sender` must be the owner of the `_requestId` request - `_requestId` request must exist, be finalized and not claimed ::: ### findCheckpointHints() Returns an array of hints for the given `_requestIds` searching among the checkpoints with indices in the range `[_firstIndex, _lastIndex]`. ```sol function findCheckpointHints(uint256[] _requestIds, uint256 _firstIndex, uint256 _lastIndex) view returns (uint256[] hintIds) ``` :::note Requirements: - Array of request ids must be sorted - `_firstIndex` must be greater than 0, because checkpoint list is 1-based array - `_lastIndex` must be less than or equal to [`getLastCheckpointIndex()`](#getlastcheckpointindex) ::: ### isBunkerModeActive() Returns `true` if bunker mode is active. ```sol function isBunkerModeActive() view returns (bool) ``` ### bunkerModeSinceTimestamp() Returns the timestamp of the last bunker mode activation, if it's active now and `BUNKER_MODE_DISABLED_TIMESTAMP` if bunker mode is disabled (i.e., protocol in turbo mode). ```sol function bunkerModeSinceTimestamp() view returns (uint256) ``` ### getLastRequestId() Returns the id of the last request in the queue. ```sol function getLastRequestId() view returns (uint256) ``` :::note Requests are indexed from `1`, so it returns `0` if there are no requests in the queue. ::: ### getLastFinalizedRequestId() Returns the id of the last finalized request in the queue. ```sol function getLastFinalizedRequestId() view returns (uint256) ``` :::note Requests are indexed from `1`, so it returns `0` if there are no finalized requests in the queue. ::: ### getLockedEtherAmount() Returns the amount of ether on the balance locked for withdrawal and available to claim. ```sol function getLockedEtherAmount() view returns (uint256) ``` ### getLastCheckpointIndex() Returns the length of the checkpoint array. Last possible value for the hint. ```sol function getLastCheckpointIndex() view returns (uint256) ``` :::note Checkpoints are indexed from `1`, so it returns `0` if there are no checkpoints yet. ::: ### unfinalizedRequestNumber() Returns the number of unfinalized requests in the queue. ```sol function unfinalizedRequestNumber() view returns (uint256) ``` ### unfinalizedStETH() Returns the amount of `stETH` in the queue yet to be finalized. ```sol function unfinalizedStETH() view returns (uint256) ``` ### calculateFinalizationBatches() View for offchain use by the oracle daemon that calculates how many requests can be finalized within the given budget, time period, and share rate limits. Returned requests are split into batches. All requests belonging to one batch must have their share rate above or below (or equal) to the `_maxShareRate`. Below you can see an example of how 14 requests with different share rates will be split into five batches by this method: ```txt ^ share rate | | โ€ข โ€ข | โ€ข โ€ข โ€ข โ€ข โ€ข |----------------------โ€ข------ _maxShareRate | โ€ข โ€ข โ€ข โ€ข โ€ข | โ€ข +-------------------------------> requestId | 1 | 2 |3| 4 | 5 | batch number ``` ```sol function calculateFinalizationBatches( uint256 _maxShareRate, uint256 _maxTimestamp, uint256 _maxRequestsPerCall, BatchesCalculationState _state ) external view returns (BatchesCalculationState) ``` where - **`_maxShareRate`** โ€” the max share rate (ETH per share) that will be used for the finalization (1e27 precision) - **`_maxTimestamp`** โ€” the max timestamp of the request that can be finalized - **`_maxRequestsPerCall`** โ€” the max request number that can be processed per iteration - **`_state`** โ€” the current state of the calculation, represented with a `BatchesCalculationState` structure: ```sol struct BatchesCalculationState { uint256 remainingEthBudget; bool finished; uint256[MAX_BATCHES_LENGTH] batches; uint256 batchesLength; } ``` - **`remainingEthBudget`** โ€” the currently remaining amount of ether. It must be set into the whole budget of the finalization at the first call - **`finished`** โ€” the flag that is set to `true` if all requests are iterated on - **`batches`** โ€” the resulting array of batches, each represented by the id of the last request in the batch - **`batchesLength`** โ€” the length of the filled part of the `batches` array Returns the current state of the finalization batch calculation. :::note This method is designed for iterative usage under gas limits. So, in the case of the number of withdrawals are too large to iterate over in one call, one can use this method repeatedly, passing the return value as an argument for the next call as long as it returns `finished` equal to `false` ::: ### prefinalize() Checks finalization batches and calculates the required amount of ether to lock and the number of shares to burn. Designed to use during the oracle report to find the amount of ether to send along the `finalize()` call. ```sol function prefinalize(uint256[] _batches, uint256 _maxShareRate) view returns (uint256 ethToLock, uint256 sharesToBurn) ``` where - **`_batches`** โ€” finalization batches calculated off-chain using `calculateFinalizationBatches()` - **`_maxShareRate`** โ€” max share rate (ETH per share) for request finalization (1e27 precision) Returns - **`ethToLock`** โ€” the amount of ether to be sent with `finalize()` method - **`sharesToBurn`** โ€” the number of shares to be burnt to match this finalization call ## Protected methods ### Roles - **FINALIZE_ROLE** โ€” role to finalize withdrawal requests in the queue - **PAUSE_ROLE** โ€” role to pause the withdrawal on the protocol - **RESUME_ROLE** โ€” role to resume the withdrawal after being paused - **ORACLE_ROLE** โ€” role to provide required oracle-related data as the last report timestamp and if the protocol is in the bunker mode - **MANAGE_TOKEN_URI_ROLE** โ€” role to set the parameters for constructing the token URI: the base URI or `NFTDescriptor` address ### finalize() Finalize requests from the last finalized one up to `_lastRequestIdToBeFinalized` using `_maxShareRate` as a base share rate for `stETH` and passing along some ether as `msg.value`. The amount of ether to send should be precalculated by the `prefinalize()` method. Emits a `BatchMetadataUpdate` and a `WithdrawalsFinalized` events. ```sol function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) payable ``` where - **`_lastRequestIdToBeFinalized`** โ€” the last request id to finalize - **`_maxShareRate`** โ€” the max share rate (ETH per share) for the request finalization (1e27 precision) :::note Requirements: - withdrawals must not be paused - `msg.sender` must have the `FINALIZE_ROLE` assigned - `_lastRequestIdToBeFinalized` must be an existing unfinalized request id - `msg.value` must be less or equal to the sum of unfinalized `stETH` up to `_lastRequestIdToBeFinalized` ::: ### pauseFor() Pause withdrawal requests placement and finalization for particular `_duration`. Claiming finalized requests will still be available. Emits a `Paused` event. ```sol function pauseFor(uint256 _duration) onlyRole(PAUSE_ROLE) ``` where - **`_duration`** โ€” pause duration in seconds (use `PAUSE_INFINITELY` for unlimited) :::note Requirements: - `msg.sender` must have a `PAUSE_ROLE` assigned - `_duration` must not be zero - the contract must not be already paused ::: ### pauseUntil() Pause withdrawal requests placement and finalization until `_pauseUntilInclusive` timestamp. Claiming finalized requests will still be available. Emits a `Paused` event. ```sol function pauseUntil(uint256 _pauseUntilInclusive) onlyRole(PAUSE_ROLE) ``` where - **`_pauseUntilInclusive`** โ€” the `block.timestamp` to pause until (inclusive) :::note Requirements: - `msg.sender` must have a `PAUSE_ROLE` assigned - `_pauseUntilInclusive` must not be in the past - the contract must not be already paused ::: ### resume() Resumes withdrawal requests placement and finalization. The contract is deployed in a paused state and should be resumed explicitly. Emits a `Resumed` event. ```sol function resume() ``` :::note Requirements: - `msg.sender` must have a `RESUME_ROLE` assigned - the contract must not be already resumed ::: ### onOracleReport() Updates bunker mode state and last report timestamp. Emits a `BunkerModeEnabled` or a `BunkerModeDisabled` event. ```sol function onOracleReport( bool _isBunkerModeNow, uint256 _bunkerStartTimestamp, uint256 _currentReportTimestamp ) ``` where - **`_isBunkerModeNow`** โ€” is bunker mode reported by the oracle - **`_bunkerStartTimestamp`** โ€” timestamp of the bunker mode activation - **`_currentReportTimestamp`** โ€” timestamp of the current report ref slot :::note Requirements: - `msg.sender` must have an `ORACLE_ROLE` assigned - all timestamps must be in the past ::: ### setBaseUri() Sets the Base URI for computing token URI. If the `NFTDescriptor` address isn't set, the `baseURI` would be used for generating the `ERC-721` token URI. Otherwise, the `NFTDescriptor` address would be used as a first-priority method. Emits a `BaseURISet` event ```sol function setBaseURI(string _baseURI) external onlyRole(MANAGE_TOKEN_URI_ROLE) ``` where - **`_baseURI`** โ€” the base URI to derive the token URI from. Should not end on `/` :::note Reverts if `msg.sender` has no `MANAGE_TOKEN_URI_ROLE` assigned. ::: ### setNFTDescriptorAddress() Sets the address of the `NFTDescriptor` contract responsible for token URI generation. If the `NFTDescriptor` address isn't set, the `baseURI` would be used for generating the `ERC-721` token URI. Otherwise, the `NFTDescriptor` address would be used as a first-priority method. Emits a `NftDescriptorAddressSet` event. ```sol function setNFTDescriptorAddress(address _nftDescriptorAddress) onlyRole(MANAGE_TOKEN_URI_ROLE) ``` where - **`_nftDescriptorAddress`** โ€” is the address of `NFTDescriptor` contract, which must support the `INFTDescriptor` interface: ```sol interface INFTDescriptor { function constructTokenURI(uint256 _requestId) external view returns (string memory) } ``` :::note Reverts if `msg.sender` has no `MANAGE_TOKEN_URI_ROLE` assigned. ::: --- # WithdrawalVault - [Source Code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/WithdrawalVault.sol) - [Deployed Contract](https://etherscan.io/address/0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f) - Inherits [WithdrawalVaultEIP7685](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.8.9/WithdrawalVaultEIP7685.sol). Abstract contract providing base functionality for [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) execution-layer requests. ## What is WithdrawalVault A simple contract that accumulates partial and full withdrawals that come from the Beacon Chain. Its address corresponds to the Lido withdrawal credentials (both `0x01` and `0x02` types). During the accounting oracle report, the vault is emptied by Lido into the internal buffer; see [Lido contract docs](lido.md#oracle-report) for details. The vault is recoverable, so the DAO can transfer any ERC-20 and ERC-721 tokens to the treasury. WithdrawalVault submits execution-layer requests on behalf of the protocol: - [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) triggerable partial and full withdrawal requests, forwarded by the [TriggerableWithdrawalsGateway](/contracts/triggerable-withdrawals-gateway); - [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) validator consolidation requests, forwarded by the [ConsolidationGateway](/contracts/consolidation-gateway). The currently deployed version is upgradable because of anticipated Ethereum withdrawal mechanics changes. ## View methods ### getContractVersion() Returns the current contract version. ```sol function getContractVersion() returns (uint256) ``` ## Methods ### withdrawWithdrawals() Transfers the `_amount` of accumulated withdrawals to the Lido contract. :::note It can be called only by the [Lido](lido.md) contract. ::: ```sol function withdrawWithdrawals(uint256 _amount) ``` ### recoverERC20() Transfers the given amount of the ERC20-token (defined by the provided token contract address) currently belonging to the vault contract address to the Lido treasury address. Emits a `ERC20Recovered` event. ```sol function recoverERC20(address _token, uint256 _amount) external ``` #### Parameters: | Name | Type | Description | | --------- | --------- | ----------------------- | | `_token` | `address` | ERC20-compatible token | | `_amount` | `uint256` | token amount to recover | ### recoverERC721() Transfers the given tokenId of the ERC721-compatible NFT (defined by the provided token contract address) currently belonging to the vault contract address to the Lido treasury address. Emits an `ERC721Recovered` event. ```sol function recoverERC721(address _token, uint256 _tokenId) external ``` #### Parameters: | Name | Type | Description | | ---------- | --------- | ----------------------- | | `_token` | `address` | ERC721-compatible token | | `_tokenId` | `uint256` | minted token id | ### addWithdrawalRequests() Submits [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) full or partial withdrawal requests for the specified public keys. Each full withdrawal request instructs a validator to fully withdraw its stake and exit its duties as a validator. Each partial withdrawal request instructs a validator to withdraw a specified amount of ETH. :::note It can be called only by the [TriggerableWithdrawalsGateway](/contracts/triggerable-withdrawals-gateway) contract. ::: ```sol function addWithdrawalRequests(bytes[] pubkeys, uint64[] amounts) payable ``` #### Parameters: | Name | Type | Description | | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- | | `pubkeys` | `bytes[]` | 48-byte public keys of validators requesting withdrawals | | `amounts` | `uint64[]` | Amounts in gwei to be withdrawn for each corresponding public key: `0` for a full withdrawal, greater than `0` for a partial one | :::note Reverts if the caller is not `TriggerableWithdrawalsGateway`, the public key array is empty or malformed, the arrays are of unequal length, or the provided total withdrawal fee value is invalid. ::: ### addConsolidationRequests() Submits [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) consolidation requests, one per (source, target) pair. Each request instructs a source validator to consolidate its stake into the target validator. :::note It can be called only by the [ConsolidationGateway](/contracts/consolidation-gateway) contract. ::: ```sol function addConsolidationRequests(bytes[] sourcePubkeys, bytes[] targetPubkeys) payable ``` #### Parameters: | Name | Type | Description | | --------------- | --------- | ----------------------------------------------------------------- | | `sourcePubkeys` | `bytes[]` | 48-byte public keys of validators requesting the consolidation | | `targetPubkeys` | `bytes[]` | 48-byte public keys of validators receiving the consolidation | :::note Reverts if the caller is not `ConsolidationGateway`, the public key arrays are empty or malformed, the arrays are of unequal length, or the provided total consolidation fee value is invalid. ::: ### getWithdrawalRequestFee Returns fee amount required per withdrawal request. ```solidity function getWithdrawalRequestFee() public view returns (uint256); ``` #### Returns: | Name | Type | Description | | ---------------------- | --------- | ------------------------------- | | `withdrawalRequestFee` | `uint256` | Current withdrawal request fee. | ### getConsolidationRequestFee Returns fee amount required per consolidation request. ```solidity function getConsolidationRequestFee() external view returns (uint256); ``` #### Returns: | Name | Type | Description | | ------------------------- | --------- | ---------------------------------- | | `consolidationRequestFee` | `uint256` | Current consolidation request fee. | --- # wstETH - [Source Code](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.6.12/WstETH.sol) - [Deployed Contract](https://etherscan.io/token/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0) ## What is wrapped stETH (wstETH)? It's an [ERC-20](https://eips.ethereum.org/EIPS/eip-20) value-accruing token wrapper for `stETH`. Its balance does not change with each oracle report, but its value in `stETH` does. Internally, it represents the user's [share](/docs/guides/lido-tokens-integration-guide.md#steth-internals-share-mechanics) of the total supply of `stETH` tokens. ## Why use wstETH? `wstETH` is mainly used as a layer of compatibility to integrate `stETH` into other DeFi protocols, that do not support rebasable tokens, especially bridges to L2s and other chains, as rebases don't work for bridged assets by default. ## How to use wstETH? The contract can be used as a trustless wrapper that accepts stETH tokens and mints wstETH in return. When the user unwraps, the contract burns the user's `wstETH`, and sends the user locked `stETH` in return. ### Staking shortcut :::note For attaching a referral address use the [Wsteth Referral Staker contract](/contracts/wsteth-staker) ::: The user can send ETH with regular transfer to the address of the contract and get wstETH in return. The contract will send ETH to Lido submit method, staking it and wrapping the received stETH seamlessly under the hood. ## Standards Contract implements the following Ethereum standards: - [ERC-20: Token Standard](https://eips.ethereum.org/EIPS/eip-20) - [ERC-2612: Permit Extension for ERC-20 Signed Approvals](https://eips.ethereum.org/EIPS/eip-2612) - [EIP-712: Typed structured data hashing and signing](https://eips.ethereum.org/EIPS/eip-712) ## View Methods ### getWstETHByStETH() Returns amount of `wstETH` for a given amount of `stETH` ```sol function getWstETHByStETH(uint256 _stETHAmount) returns (uint256) ``` #### Parameters | Name | Type | Description | | -------------- | --------- | --------------- | | `_stETHAmount` | `uint256` | amount of stETH | ### getStETHByWstETH() Returns amount of `stETH` for a given amount of `wstETH` ```sol function getStETHByWstETH(uint256 _wstETHAmount) returns (uint256) ``` #### Parameters | Parameter Name | Type | Description | | --------------- | --------- | ---------------- | | `_wstETHAmount` | `uint256` | amount of wstETH | ### stEthPerToken() Returns the amount of stETH tokens corresponding to one `wstETH` ```sol function stEthPerToken() returns (uint256) ``` ### tokensPerStEth() Returns the number of `wstETH` tokens corresponding to one `stETH` ```sol function tokensPerStEth() returns (uint256) ``` ## Methods ### wrap() Exchanges `stETH` to `wstETH` ```sol function wrap(uint256 _stETHAmount) returns (uint256) ``` :::note Requirements: - `_stETHAmount` must be non-zero - `msg.sender` must approve at least `_stETHAmount` stETH to this contract. - `msg.sender` must have at least `_stETHAmount` of stETH. ::: #### Parameters | Parameter Name | Type | Description | | -------------- | --------- | ---------------------------------------------- | | `_stETHAmount` | `uint256` | amount of stETH to wrap in exchange for wstETH | #### Returns Amount of wstETH user receives after wrap ### unwrap() Exchanges wstETH to `stETH` ```sol function unwrap(uint256 _wstETHAmount) returns (uint256) ``` :::note Requirements: - `_wstETHAmount` must be non-zero - `msg.sender` must have at least `_wstETHAmount` wstETH. ::: #### Parameters | Parameter Name | Type | Description | | --------------- | --------- | ------------------------------------------------ | | `_wstETHAmount` | `uint256` | amount of wstETH to unwrap in exchange for stETH | #### Returns Amount of stETH user receives after unwrapping ### receive() Shortcut to stake ETH and auto-wrap returned `stETH` ```sol receive() payable ``` --- # wstETHReferralStaker - [Source code](https://github.com/lidofinance/si-lidity/blob/develop/si-contracts/0.8.25/WstETHReferralStaker.sol) - [Deployed Contract](https://etherscan.io/address/0xa88f0329C2c4ce51ba3fc619BBf44efE7120Dd0d) ## What is wstETH Referral Staker **WstETHReferralStaker** is an utility contract that allows users to stake ETH into the Lido protocol with referral address, then automatically wrap the received stETH into wstETH and transfer it back to the user in a single transaction. ## Upgradability This contract is **non-upgradable**, **immutable** and **permissionless**. ## How to use this contract? :::warning Do not send Ether or any tokens directly to this contract address. No funds can be rescued from this contract. ::: Call the `stakeETH(address _referral)` method on the `wstETHReferralStaker` contract with `value` equal to the amount of ETH you want to stake with a `_referral` address set to the preferred referral address(could be a zero address). For more information see [`stETH.submit(address _referral)`](/contracts/lido#submit) ## Methods Stake ETH directly into wstETH with a `referral` address. :::note To preview a total amount of wstETH tokens to be staked, the `eth_call` RPC method can be used assuming the same `msg.value` passed. ::: ```solidity function stakeETH(address _referral) external payable returns (uint256) ``` **Parameters** | Parameter Name | Type | Description | | -------------- | --------- | --------------------------------- | | `msg.value` | `uint256` | ETH value attached to transaction | | `_referral` | `address` | Referral address | **Returns** Amount of wstETH caller receives after wrap. --- # ๐ŸŒ Mainnet {#mainnet} :::info **Production Lido Protocol Deployment** This page lists production contract addresses on mainnets, including Ethereum and other networks where the protocol and its components are deployed. **Deployment Information:** - โš“ Lido protocol version: [**`v4.0.1`**](https://github.com/lidofinance/core/releases/tag/v4.0.1) - ๐ŸŒ Network: Ethereum Mainnet (Chain ID: `1`) - โœ… Status: Active and maintained ::: ## ๐Ÿ›๏ธ Core Protocol {#core-protocol} - Lido Locator: [`0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb`](https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb) (proxy) - \[[proposed to remove](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] Lido Locator: [`0xF2Ffb952e129a63F0614Ff87126E1d4a494A2313`](https://etherscan.io/address/0xF2Ffb952e129a63F0614Ff87126E1d4a494A2313) (impl) - \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] Lido Locator: [`0x60E09F1791F1168d0450E4F100616B4a3F95119C`](https://etherscan.io/address/0x60E09F1791F1168d0450E4F100616B4a3F95119C) (impl) - Lido and stETH token: [`0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84`](https://etherscan.io/address/0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) (proxy) - Lido: [`0x028271E30a695c0527A0C50cA30603feD004cDb0`](https://etherscan.io/address/0x028271E30a695c0527A0C50cA30603feD004cDb0) (impl) - Accounting: [`0x23ED611be0e1a820978875C0122F92260804cdDf`](https://etherscan.io/address/0x23ED611be0e1a820978875C0122F92260804cdDf) (proxy) - Accounting: [`0x3aa937Ac2ab89CDd363EdC6b5A4d4A42dF5bc043`](https://etherscan.io/address/0x3aa937Ac2ab89CDd363EdC6b5A4d4A42dF5bc043) (impl) - wstETH token: [`0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0`](https://etherscan.io/address/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0) - wstETH referral staker: [`0xa88f0329C2c4ce51ba3fc619BBf44efE7120Dd0d`](https://etherscan.io/address/0xa88f0329C2c4ce51ba3fc619BBf44efE7120Dd0d) - EIP-712 helper for stETH: [`0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7`](https://etherscan.io/address/0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7) - Staking Router: [`0xFdDf38947aFB03C621C71b06C9C70bce73f12999`](https://etherscan.io/address/0xFdDf38947aFB03C621C71b06C9C70bce73f12999) (proxy) - Staking Router: [`0xDD76927045435C7605cf6f5F978cfb8CABDb5F80`](https://etherscan.io/address/0xDD76927045435C7605cf6f5F978cfb8CABDb5F80) (impl) - SR Library: [`0xc0be9942Fd8f54aB126A5F0Ba649A90049ccad14`](https://etherscan.io/address/0xc0be9942Fd8f54aB126A5F0Ba649A90049ccad14) (external lib) - Beacon Chain Depositor: [`0xf98AC162eAB766bDB9507c3584c00C535B8F6216`](https://etherscan.io/address/0xf98AC162eAB766bDB9507c3584c00C535B8F6216) - \[[proposed to remove](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] Deposit Security Module: [`0xF573E9E3de1f86B085417ab294f56E7920B4e9Be`](https://etherscan.io/address/0xF573E9E3de1f86B085417ab294f56E7920B4e9Be) - \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/25)\] Deposit Security Module: [`0x39BB5d491e98A44D1bfe8047A737a81E296a63E0`](https://etherscan.io/address/0x39BB5d491e98A44D1bfe8047A737a81E296a63E0) - Execution Layer Rewards Vault: [`0x388C818CA8B9251b393131C08a736A67ccB19297`](https://etherscan.io/address/0x388C818CA8B9251b393131C08a736A67ccB19297) - Withdrawal Queue ERC721: [`0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1`](https://etherscan.io/address/0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1) (proxy) - Withdrawal Vault: [`0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f`](https://etherscan.io/address/0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f) (proxy) - Withdrawal Vault: [`0xfB4521BD151BFB45DB6045D2d07e58e0f597e340`](https://etherscan.io/address/0xfB4521BD151BFB45DB6045D2d07e58e0f597e340) (impl) - Burner: [`0xE76c52750019b80B43E36DF30bf4060EB73F573a`](https://etherscan.io/address/0xE76c52750019b80B43E36DF30bf4060EB73F573a) (proxy) - Burner: [`0xEe1E3B4f047122650086985f794f0dB5f10Ae49D`](https://etherscan.io/address/0xEe1E3B4f047122650086985f794f0dB5f10Ae49D) (impl) - MEV Boost Relay Allowed List: [`0xF95f069F9AD107938F6ba802a3da87892298610E`](https://etherscan.io/address/0xf95f069f9ad107938f6ba802a3da87892298610e) - Min First Allocation Strategy: [`0x98F1da239a199574A4F7f371fD8fc0a872022c73`](https://etherscan.io/address/0x98F1da239a199574A4F7f371fD8fc0a872022c73) (external lib) - Triggerable Withdrawals Gateway: [`0xDC00116a0D3E064427dA2600449cfD2566B3037B`](https://etherscan.io/address/0xDC00116a0D3E064427dA2600449cfD2566B3037B) - Top Up Gateway: [`0x3FC2C71579D80790Aaa3fc7Be8B66ac39dC57374`](https://etherscan.io/address/0x3FC2C71579D80790Aaa3fc7Be8B66ac39dC57374) (proxy) - Top Up Gateway: [`0xb08dBc68C521cD7A4318dc4C807a42bEB20f1106`](https://etherscan.io/address/0xb08dBc68C521cD7A4318dc4C807a42bEB20f1106) (impl) - Validator Exit Delay Verifier: [`0xbDb567672c867DB533119C2dcD4FB9d8b44EC82f`](https://etherscan.io/address/0xbDb567672c867DB533119C2dcD4FB9d8b44EC82f) - Vault Hub: [`0x1d201BE093d847f6446530Efb0E8Fb426d176709`](https://etherscan.io/address/0x1d201BE093d847f6446530Efb0E8Fb426d176709) (proxy) - Vault Hub: [`0x6330fE7756FBE8649adfb9A541d61C5edB8B4D70`](https://etherscan.io/address/0x6330fE7756FBE8649adfb9A541d61C5edB8B4D70) (impl) - Predeposit Guarantee: [`0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3`](https://etherscan.io/address/0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3) (proxy) - Predeposit Guarantee: [`0xE78717192C45736DF0E4be55c0219Ee7f9aDdd0D`](https://etherscan.io/address/0xE78717192C45736DF0E4be55c0219Ee7f9aDdd0D) (impl) - Operator Grid: [`0xC69685E89Cefc327b43B7234AC646451B27c544d`](https://etherscan.io/address/0xC69685E89Cefc327b43B7234AC646451B27c544d) (proxy) - Operator Grid: [`0xA612E30D71d7D54aEaf4e5A21023F3F270932C2C`](https://etherscan.io/address/0xA612E30D71d7D54aEaf4e5A21023F3F270932C2C) (impl) ### ๐Ÿ”จ stVaults Factory Stack {#stvaults-factory-stack} - Staking Vault Factory: [`0x02Ca7772FF14a9F6c1a08aF385aA96bb1b34175A`](https://etherscan.io/address/0x02Ca7772FF14a9F6c1a08aF385aA96bb1b34175A) - Staking Vault Beacon: [`0x5FbE8cEf9CCc56ad245736D3C5bAf82ad54Ca789`](https://etherscan.io/address/0x5FbE8cEf9CCc56ad245736D3C5bAf82ad54Ca789) - Staking Vault Implementation: [`0x06A56487494aa080deC7Bf69128EdA9225784553`](https://etherscan.io/address/0x06A56487494aa080deC7Bf69128EdA9225784553) - Dashboard Implementation: [`0x294825c2764c7D412dc32d87E2242c4f1D989AF3`](https://etherscan.io/address/0x294825c2764c7D412dc32d87E2242c4f1D989AF3) - Validator Consolidation Requests: [`0xaC4Aae7123248684C405A4b0038C1560EC7fE018`](https://etherscan.io/address/0xaC4Aae7123248684C405A4b0038C1560EC7fE018) ### ๐ŸŒŠ DeFi Wrapper {#defi-wrapper} - DeFi Wrapper Factory: [`0x3f221b8E5bC098cC6C23611BEeacaeCfD77e1587`](https://etherscan.io/address/0x3f221b8E5bC098cC6C23611BEeacaeCfD77e1587) - Lido Earn Strategy Factory: [`0x8Fac09FD82F031D390B94622E2E4baBf16Fd2236`](https://etherscan.io/address/0x8Fac09FD82F031D390B94622E2E4baBf16Fd2236) ### ๐Ÿ”— Consolidation Stack {#consolidation-stack} - Consolidation Migrator: [`0x9Dc70b5A4f4F5E4AF9058C983D560564F031f1D7`](https://etherscan.io/address/0x9Dc70b5A4f4F5E4AF9058C983D560564F031f1D7) (proxy) - Consolidation Migrator: [`0x6Fb4c152F092373dD71f0C07C83c1E77406599aB`](https://etherscan.io/address/0x6Fb4c152F092373dD71f0C07C83c1E77406599aB) (impl) - Consolidation Bus: [`0xd907CE33B4Be423823d1CFFe80BD147E8b8554C8`](https://etherscan.io/address/0xd907CE33B4Be423823d1CFFe80BD147E8b8554C8) (proxy) - Consolidation Bus: [`0xFfDe8Acab9D7037f29198Ad03ad6d05bac8B0a2E`](https://etherscan.io/address/0xFfDe8Acab9D7037f29198Ad03ad6d05bac8B0a2E) (impl) - Consolidation Gateway: [`0x17be979344f2c2cC806229a532D92f8742C10462`](https://etherscan.io/address/0x17be979344f2c2cC806229a532D92f8742C10462) ## ๐Ÿ”ฎ Oracle Contracts {#oracle-contracts} - Accounting Oracle: - AccountingOracle: [`0x852deD011285fe67063a08005c71a85690503Cee`](https://etherscan.io/address/0x852deD011285fe67063a08005c71a85690503Cee) (proxy) - AccountingOracle: [`0xe4f03D1107d1905B6F2A28FCb6Af221E0CE19136`](https://etherscan.io/address/0xe4f03D1107d1905B6F2A28FCb6Af221E0CE19136) (impl) - HashConsensus: [`0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288`](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288) - Validators Exit Bus Oracle: - ValidatorsExitBusOracle: [`0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e`](https://etherscan.io/address/0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e) (proxy) - ValidatorsExitBusOracle: [`0x2C3386b39db89eef0F362A3BE0C05a6811E809E3`](https://etherscan.io/address/0x2C3386b39db89eef0F362A3BE0C05a6811E809E3) (impl) - HashConsensus: [`0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a`](https://etherscan.io/address/0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a) - OracleReportSanityChecker: [`0x147f8d3cf3004FAf9Bf94E88B54b6C06De507be9`](https://etherscan.io/address/0x147f8d3cf3004FAf9Bf94E88B54b6C06De507be9) - OracleDaemonConfig: [`0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09`](https://etherscan.io/address/0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09) - Lazy Oracle: [`0x5DB427080200c235F2Ae8Cd17A7be87921f7AD6c`](https://etherscan.io/address/0x5DB427080200c235F2Ae8Cd17A7be87921f7AD6c) (proxy) - Lazy Oracle: [`0x96c9a897D116ef660086d3aA67b3af653324aB37`](https://etherscan.io/address/0x96c9a897D116ef660086d3aA67b3af653324aB37) (impl) ## ๐Ÿ”‘ Execution Delegation Framework {#execution-delegation-framework} - \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/10)\] DelegationFactory: [`0xD990770eB2B4b6062EDdB06892fF179C693b46e6`](https://etherscan.io/address/0xD990770eB2B4b6062EDdB06892fF179C693b46e6) ## ๐Ÿ—ณ๏ธ DAO Contracts {#dao-contracts} - Lido DAO (Kernel): [`0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc`](https://etherscan.io/address/0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc) (proxy) - LDO token: [`0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32`](https://etherscan.io/address/0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32) - Aragon Voting: [`0x2e59A20f205bB85a89C53f1936454680651E618e`](https://etherscan.io/address/0x2e59A20f205bB85a89C53f1936454680651E618e) (proxy) - Aragon Token Manager: [`0xf73a1260d222f447210581DDf212D915c09a3249`](https://etherscan.io/address/0xf73a1260d222f447210581DDf212D915c09a3249) (proxy) - Aragon Finance: [`0xB9E5CBB9CA5b0d659238807E84D0176930753d86`](https://etherscan.io/address/0xB9E5CBB9CA5b0d659238807E84D0176930753d86) (proxy) - Aragon Agent: [`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c) (proxy) - Aragon ACL: [`0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb`](https://etherscan.io/address/0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb) (proxy) - EVMScriptRegistry: [`0x853cc0D5917f49B57B8e9F89e491F5E18919093A`](https://etherscan.io/address/0x853cc0D5917f49B57B8e9F89e491F5E18919093A) (proxy) - Aragon PM: [`0x0cb113890b04b49455dfe06554e2d784598a29c9`](https://etherscan.io/address/0x0cb113890b04b49455dfe06554e2d784598a29c9) (proxy) - Voting Repo: [`0x4ee3118e3858e8d7164a634825bfe0f73d99c792`](https://etherscan.io/address/0x4ee3118e3858e8d7164a634825bfe0f73d99c792) (proxy) - Lido App Repo: [`0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1`](https://etherscan.io/address/0xF5Dc67E54FC96F993CD06073f71ca732C1E654B1) (proxy) - Node Operators Registry Repo: [`0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831`](https://etherscan.io/address/0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831) (proxy) - Simple DVT Repo: [`0x2325b0a607808dE42D918DB07F925FFcCfBb2968`](https://etherscan.io/address/0x2325b0a607808dE42D918DB07F925FFcCfBb2968) (proxy) - Reserve Fund: [`0x8B3f33234ABD88493c0Cd28De33D583B70beDe35`](https://etherscan.io/address/0x8B3f33234ABD88493c0Cd28De33D583B70beDe35) ### ๐Ÿงฌ Dual Governance {#dual-governance} - Emergency Protected Timelock: [`0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316`](https://etherscan.io/address/0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316) - Emergency activation committee: [`0x8B7854488Fde088d686Ea672B6ba1A5242515f45`](https://etherscan.io/address/0x8B7854488Fde088d686Ea672B6ba1A5242515f45) - Emergency execution committee: [`0xC7792b3F2B399bB0EdF53fECDceCeB97FBEB18AF`](https://etherscan.io/address/0xC7792b3F2B399bB0EdF53fECDceCeB97FBEB18AF) - Admin Executor: [`0x23E0B465633FF5178808F4A75186E2F2F9537021`](https://etherscan.io/address/0x23E0B465633FF5178808F4A75186E2F2F9537021) - Dual Governance: [`0xC1db28B3301331277e307FDCfF8DE28242A4486E`](https://etherscan.io/address/0xC1db28B3301331277e307FDCfF8DE28242A4486E) - Dual Governance Config Provider: [`0xa1692Af6FDfdD1030E4E9c4Bc429986FA64CB5EF`](https://etherscan.io/address/0xa1692Af6FDfdD1030E4E9c4Bc429986FA64CB5EF) - Emergency Governance: [`0x553337946F2FAb8911774b20025fa776B76a7CcE`](https://etherscan.io/address/0x553337946F2FAb8911774b20025fa776B76a7CcE) - Veto Signaling Escrow: [`0x165813A31446a98c84E20Dda8C101BB3C8228e1c`](https://etherscan.io/address/0x165813A31446a98c84E20Dda8C101BB3C8228e1c) (proxy) - Veto Signaling Escrow: [`0xd6A67636c05BeB5B4a5c90D408b03A63c4e39426`](https://etherscan.io/address/0xd6A67636c05BeB5B4a5c90D408b03A63c4e39426) (impl) - Reseal Manager: [`0x7914b5a1539b97Bd0bbd155757F25FD79A522d24`](https://etherscan.io/address/0x7914b5a1539b97Bd0bbd155757F25FD79A522d24) - Reseal committee: [`0xFFe21561251c49AdccFad065C94Fb4931dF49081`](https://etherscan.io/address/0xFFe21561251c49AdccFad065C94Fb4931dF49081) - Tiebreaker Core Committee: [`0xf65614d73952Be91ce0aE7Dd9cFf25Ba15bEE2f5`](https://etherscan.io/address/0xf65614d73952Be91ce0aE7Dd9cFf25Ba15bEE2f5) - Tiebreaker Sub Committees: - Builders Sub Committee [`0x3D3ba54D54bbFF40F2Dfa2A8e27bD4dE3dab2951`](https://etherscan.io/address/0x3D3ba54D54bbFF40F2Dfa2A8e27bD4dE3dab2951) - Node Operators Sub Committee [`0xDBfa0B8A15a503f25224fcA5F84a3853230A715C`](https://etherscan.io/address/0xDBfa0B8A15a503f25224fcA5F84a3853230A715C) - Ethereum Ecosystem Sub Committee [`0xBF048f2111497B6Df5E062811f5fC422804D4baE`](https://etherscan.io/address/0xBF048f2111497B6Df5E062811f5fC422804D4baE) - Time Constraints: [`0x2a30F5aC03187674553024296bed35Aa49749DDa`](https://etherscan.io/address/0x2a30F5aC03187674553024296bed35Aa49749DDa) ## ๐Ÿ”Œ CircuitBreaker {#circuit-breaker} - CircuitBreaker: [`0x6019CB557978296BA3C08a7B73225C0975DFB2F7`](https://etherscan.io/address/0x6019CB557978296BA3C08a7B73225C0975DFB2F7) ### Covered pausables and their pausers Each pausable contract below is covered by the CircuitBreaker, with a designated pauser authorized to trigger a pause. The pausers are the **[CircuitBreaker Committee](#emergency-brakes-multisigs)** for core protocol pausables, the **[CSM Committee](#committees)** for CSM pausables, and the **[CMC Committee](#committees)** for CMv2 pausables. | Pausable | Pauser | | --- | --- | | [Withdrawal Queue ERC721](https://etherscan.io/address/0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [Validators Exit Bus Oracle](https://etherscan.io/address/0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [Triggerable Withdrawals Gateway](https://etherscan.io/address/0xDC00116a0D3E064427dA2600449cfD2566B3037B) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [Vault Hub](https://etherscan.io/address/0x1d201BE093d847f6446530Efb0E8Fb426d176709) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [Predeposit Guarantee](https://etherscan.io/address/0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [Consolidation Gateway](https://etherscan.io/address/0x17be979344f2c2cC806229a532D92f8742C10462) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [Top Up Gateway](https://etherscan.io/address/0x3FC2C71579D80790Aaa3fc7Be8B66ac39dC57374) | [CircuitBreaker Committee](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | | [CSModule](https://etherscan.io/address/0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [CSM Accounting](https://etherscan.io/address/0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [CSM FeeOracle](https://etherscan.io/address/0x4D4074628678Bd302921c20573EEa1ed38DdF7FB) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [CSM Verifier](https://etherscan.io/address/0xfce7aB839e55de77730716D05b3553e45ab3A5Ba) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [CSM Ejector](https://etherscan.io/address/0x610B517D380f287c239C93F8eF6FfBd567AA4bA5) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [VettedGate (Identified Community Stakers Gate)](https://etherscan.io/address/0xB314D4A76C457c93150d308787939063F4Cc67E0) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [VettedGate (Identified DVT Cluster Gate)](https://etherscan.io/address/0xa12760721A72A7199aB38059DA6690b9Cd4ed7B8) | [CSM Committee](https://etherscan.io/address/0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) | | [CuratedModule](https://etherscan.io/address/0xDa5F930cE326EB5205085D66c72A4E79d60cB8C1) | [CMC Committee](https://etherscan.io/address/0x2570e0b22AD904501dfB0d49575991ACB801dD91) | | [Curated Accounting](https://etherscan.io/address/0x2F91e3A8C5d6593bf4F8403fCfeCcd62dF59f6F6) | [CMC Committee](https://etherscan.io/address/0x2570e0b22AD904501dfB0d49575991ACB801dD91) | | [Curated FeeOracle](https://etherscan.io/address/0x8EeFCdbD984c30E472BcbF545783D051CB5114e5) | [CMC Committee](https://etherscan.io/address/0x2570e0b22AD904501dfB0d49575991ACB801dD91) | | [Curated Verifier](https://etherscan.io/address/0xC392F457960f1B13Ebaf1aa6C065479dD507E1E3) | [CMC Committee](https://etherscan.io/address/0x2570e0b22AD904501dfB0d49575991ACB801dD91) | | [Curated Ejector](https://etherscan.io/address/0xe181A377A2d2BDE9A83f1474BC3DB7A412de091E) | [CMC Committee](https://etherscan.io/address/0x2570e0b22AD904501dfB0d49575991ACB801dD91) | ## ๐Ÿ“Š Data Bus {#data-bus} - DataBus on Gnosis Chain: [`0x37De961D6bb5865867aDd416be07189D2Dd960e6`](https://gnosis.blockscout.com/address/0x37De961D6bb5865867aDd416be07189D2Dd960e6) - DataBus on Base: [`0x37De961D6bb5865867aDd416be07189D2Dd960e6`](https://basescan.org/address/0x37De961D6bb5865867aDd416be07189D2Dd960e6) - DataBus on Optimism: [`0x37De961D6bb5865867aDd416be07189D2Dd960e6`](https://optimistic.etherscan.io/address/0x37De961D6bb5865867aDd416be07189D2Dd960e6) - DataBus on Polygon PoS: [`0x37De961D6bb5865867aDd416be07189D2Dd960e6`](https://polygonscan.com/address/0x37De961D6bb5865867aDd416be07189D2Dd960e6) ## ๐Ÿ”„ Post Token Rebase Receiver {#post-token-rebase-receiver} - Token Rate Notifier: [`0xbe05d12Fd10919F1881125006523452F6aFF791b`](https://etherscan.io/address/0xbe05d12Fd10919F1881125006523452F6aFF791b) ## ๐Ÿงฉ Staking Modules {#staking-modules} ### ๐Ÿ”’ Curated Module v1 {#curated-module} - Node Operators Registry: [`0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5`](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5) (proxy) - Node Operators Registry: [`0x6828b023e737f96B168aCd0b5c6351971a4F81aE`](https://etherscan.io/address/0x6828b023e737f96B168aCd0b5c6351971a4F81aE) (impl) ### โ˜€๏ธ Simple DVT Module {#simple-dvt-module} - Node Operators Registry: [`0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433`](https://etherscan.io/address/0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433) (proxy) - Node Operators Registry: [`0x6828b023e737f96B168aCd0b5c6351971a4F81aE`](https://etherscan.io/address/0x6828b023e737f96B168aCd0b5c6351971a4F81aE) (impl) ### ๐Ÿ•ถ๏ธ Community Staking Module {#community-staking-module} - Entry Gates: - PermissionlessGate: [`0xb8cd8F059Ad7a5dB8CAfDe34aAb007317F7156C8`](https://etherscan.io/address/0xb8cd8F059Ad7a5dB8CAfDe34aAb007317F7156C8) - VettedGate (Identified Community Stakers Gate): [`0xB314D4A76C457c93150d308787939063F4Cc67E0`](https://etherscan.io/address/0xB314D4A76C457c93150d308787939063F4Cc67E0) (proxy) - VettedGate (Identified DVT Cluster Gate): [`0xa12760721A72A7199aB38059DA6690b9Cd4ed7B8`](https://etherscan.io/address/0xa12760721A72A7199aB38059DA6690b9Cd4ed7B8) (proxy) - VettedGate implementation (for all gates): [`0x66ADb8b3F58d3DFdF6bAdB595E41f19e947E5c14`](https://etherscan.io/address/0x66ADb8b3F58d3DFdF6bAdB595E41f19e947E5c14) - CSModule: [`0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F`](https://etherscan.io/address/0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F) (proxy) - CSModule: [`0x63992a86f009fcC796a8369feEfB68880aef4e3a`](https://etherscan.io/address/0x63992a86f009fcC796a8369feEfB68880aef4e3a) (impl) - Accounting: [`0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da`](https://etherscan.io/address/0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da) (proxy) - Accounting: [`0xe768572cc5aE5C698345C59288d871a949Ea8bd3`](https://etherscan.io/address/0xe768572cc5aE5C698345C59288d871a949Ea8bd3) (impl) - ParametersRegistry: [`0x9D28ad303C90DF524BA960d7a2DAC56DcC31e428`](https://etherscan.io/address/0x9D28ad303C90DF524BA960d7a2DAC56DcC31e428) (proxy) - ParametersRegistry: [`0x107d287F178cD54792614d7D63C47D8242240BeD`](https://etherscan.io/address/0x107d287F178cD54792614d7D63C47D8242240BeD) (impl) - FeeDistributor: [`0xD99CC66fEC647E68294C6477B40fC7E0F6F618D0`](https://etherscan.io/address/0xD99CC66fEC647E68294C6477B40fC7E0F6F618D0) (proxy) - FeeDistributor: [`0x936da7cDB7eed1084d294E23eA1d7Ad72DCcfE0E`](https://etherscan.io/address/0x936da7cDB7eed1084d294E23eA1d7Ad72DCcfE0E) (impl) - Verifier: [`0xfce7aB839e55de77730716D05b3553e45ab3A5Ba`](https://etherscan.io/address/0xfce7aB839e55de77730716D05b3553e45ab3A5Ba) - FeeOracle: - FeeOracle: [`0x4D4074628678Bd302921c20573EEa1ed38DdF7FB`](https://etherscan.io/address/0x4D4074628678Bd302921c20573EEa1ed38DdF7FB) (proxy) - FeeOracle: [`0xecE6e0Cde61078F76b66Ef0C338a6875E5D01F79`](https://etherscan.io/address/0xecE6e0Cde61078F76b66Ef0C338a6875E5D01F79) (impl) - HashConsensus: [`0x71093efF8D8599b5fA340D665Ad60fA7C80688e4`](https://etherscan.io/address/0x71093efF8D8599b5fA340D665Ad60fA7C80688e4) - ValidatorStrikes: [`0xaa328816027F2D32B9F56d190BC9Fa4A5C07637f`](https://etherscan.io/address/0xaa328816027F2D32B9F56d190BC9Fa4A5C07637f) (proxy) - ValidatorStrikes: [`0xd25E7C3923d2e68c325980b0e15eD20d62B2691F`](https://etherscan.io/address/0xd25E7C3923d2e68c325980b0e15eD20d62B2691F) (impl) - Ejector: [`0x610B517D380f287c239C93F8eF6FfBd567AA4bA5`](https://etherscan.io/address/0x610B517D380f287c239C93F8eF6FfBd567AA4bA5) - ExitPenalties: [`0x06cd61045f958A209a0f8D746e103eCc625f4193`](https://etherscan.io/address/0x06cd61045f958A209a0f8D746e103eCc625f4193) (proxy) - ExitPenalties: [`0xA5b9e96E951089E629Ab0834AEaF242a81394EA0`](https://etherscan.io/address/0xA5b9e96E951089E629Ab0834AEaF242a81394EA0) (impl) - Factories: - VettedGateFactory: [`0xc0f110Af6eA9119037a71C84D14506A22AE43DdE`](https://etherscan.io/address/0xc0f110Af6eA9119037a71C84D14506A22AE43DdE) - External libraries: - AssetRecovererLib: [`0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902`](https://etherscan.io/address/0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902) - BondCurvesLib: [`0xC4511d09639e5E174506083443da230D39196323`](https://etherscan.io/address/0xC4511d09639e5E174506083443da230D39196323) - DepositQueueOps: [`0xb430AA6C70A352c2aaC9813AE049A210dB11aB41`](https://etherscan.io/address/0xb430AA6C70A352c2aaC9813AE049A210dB11aB41) - GeneralPenalty: [`0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4`](https://etherscan.io/address/0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4) - NOAddresses: [`0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3`](https://etherscan.io/address/0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3) - NodeOperatorOps: [`0xDD42EE5D54A1822021782F3F455bb99fBC19499A`](https://etherscan.io/address/0xDD42EE5D54A1822021782F3F455bb99fBC19499A) - StakeTracker: [`0xbb6E4Db18182d45038F91B9F1195291c206fd8d2`](https://etherscan.io/address/0xbb6E4Db18182d45038F91B9F1195291c206fd8d2) - TopUpQueueOps: [`0xdA104f5f2a18405fC7cCD6E0A7FEB5B824843606`](https://etherscan.io/address/0xdA104f5f2a18405fC7cCD6E0A7FEB5B824843606) - WithdrawnValidatorLib: [`0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A`](https://etherscan.io/address/0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A) #### ๐Ÿ› ๏ธ Community Staking Module V3 Upgrade (temporary) {#csm3-upgrade-temporary} - Identified DVT Cluster Curve Setup: [`0x711985E069f4d702e0457C0dACAde3D3894Ce4E3`](https://etherscan.io/address/0x711985E069f4d702e0457C0dACAde3D3894Ce4E3) ### ๐Ÿ‘” Curated Module v2 {#curated-module-v2} - Entry Gates: - CuratedGate (Professional Operator Gate): [`0x6093EFA6B5E2FF3be54d1c895c9deA932805c49F`](https://etherscan.io/address/0x6093EFA6B5E2FF3be54d1c895c9deA932805c49F) (proxy) - CuratedGate (Professional Trusted Operator Gate): [`0x8c002c6eE10cf8adb78D1F9EB2e134FdaF8A7C1a`](https://etherscan.io/address/0x8c002c6eE10cf8adb78D1F9EB2e134FdaF8A7C1a) (proxy) - CuratedGate (Public Good Operator Gate): [`0x207798e6fD1aa7Ee8a63782A64c959cD6727b78C`](https://etherscan.io/address/0x207798e6fD1aa7Ee8a63782A64c959cD6727b78C) (proxy) - CuratedGate (Decentralization Operator Gate): [`0xeF273Ca4A21Ba7B414Ae3C9f9b443038cb133F72`](https://etherscan.io/address/0xeF273Ca4A21Ba7B414Ae3C9f9b443038cb133F72) (proxy) - CuratedGate (Extra Effort Operator Gate): [`0x3BbBb175f7F07954DE00052b20E1c5572223F24D`](https://etherscan.io/address/0x3BbBb175f7F07954DE00052b20E1c5572223F24D) (proxy) - CuratedGate (Intra-Operator DVT Cluster Gate): [`0x86A8d4E0db5938D21d98047544668FCCB1A9ADc8`](https://etherscan.io/address/0x86A8d4E0db5938D21d98047544668FCCB1A9ADc8) (proxy) - CuratedGate (Intra-Operator DVT Cluster Plus Gate): [`0x773933F9db8964A17d62fb808f2EC7A2de4247CC`](https://etherscan.io/address/0x773933F9db8964A17d62fb808f2EC7A2de4247CC) (proxy) - CuratedGate implementation (for all gates): [`0x3cb948FD454ad6b20DE67633f25DcbDbEaa0e849`](https://etherscan.io/address/0x3cb948FD454ad6b20DE67633f25DcbDbEaa0e849) - CuratedModule: [`0xDa5F930cE326EB5205085D66c72A4E79d60cB8C1`](https://etherscan.io/address/0xDa5F930cE326EB5205085D66c72A4E79d60cB8C1) (proxy) - CuratedModule: [`0x959fC67FE53c8A6C7a1AEd73430Aa07a36eD9337`](https://etherscan.io/address/0x959fC67FE53c8A6C7a1AEd73430Aa07a36eD9337) (impl) - MetaRegistry: [`0xA64b339eebD3dC3De848298B6a140955932901d8`](https://etherscan.io/address/0xA64b339eebD3dC3De848298B6a140955932901d8) (proxy) - MetaRegistry: [`0x6d852907463496622bb5FE5bc55cc30C4682E10e`](https://etherscan.io/address/0x6d852907463496622bb5FE5bc55cc30C4682E10e) (impl) - Accounting: [`0x2F91e3A8C5d6593bf4F8403fCfeCcd62dF59f6F6`](https://etherscan.io/address/0x2F91e3A8C5d6593bf4F8403fCfeCcd62dF59f6F6) (proxy) - Accounting: [`0xB41F5d2721906b3BE4fC7ae08261266C801076C2`](https://etherscan.io/address/0xB41F5d2721906b3BE4fC7ae08261266C801076C2) (impl) - ParametersRegistry: [`0xffC1C5d59CeAC6F6c27E701F04a70cb50474607C`](https://etherscan.io/address/0xffC1C5d59CeAC6F6c27E701F04a70cb50474607C) (proxy) - ParametersRegistry: [`0xfF419BbBC5f44d46547079922a88d691886d192a`](https://etherscan.io/address/0xfF419BbBC5f44d46547079922a88d691886d192a) (impl) - FeeDistributor: [`0x367d23c756599c20DCc8D6943F4976E8F88D60d7`](https://etherscan.io/address/0x367d23c756599c20DCc8D6943F4976E8F88D60d7) (proxy) - FeeDistributor: [`0x7C8FEE1dcC95Df60fC9b5BE7603c28Eb3af16753`](https://etherscan.io/address/0x7C8FEE1dcC95Df60fC9b5BE7603c28Eb3af16753) (impl) - Verifier: [`0xC392F457960f1B13Ebaf1aa6C065479dD507E1E3`](https://etherscan.io/address/0xC392F457960f1B13Ebaf1aa6C065479dD507E1E3) - FeeOracle: - FeeOracle: [`0x8EeFCdbD984c30E472BcbF545783D051CB5114e5`](https://etherscan.io/address/0x8EeFCdbD984c30E472BcbF545783D051CB5114e5) (proxy) - FeeOracle: [`0x16804084408B6Caad10046F49aD421cBA56C0b5e`](https://etherscan.io/address/0x16804084408B6Caad10046F49aD421cBA56C0b5e) (impl) - HashConsensus: [`0x902D64c93F6595339aA46105627a085591051aFb`](https://etherscan.io/address/0x902D64c93F6595339aA46105627a085591051aFb) - ValidatorStrikes: [`0xf4618370a1fBf46905B16C10817c8CFaD924D6db`](https://etherscan.io/address/0xf4618370a1fBf46905B16C10817c8CFaD924D6db) (proxy) - ValidatorStrikes: [`0x239Ee6ab18fB06370ad53Ce05097B32208fB0a30`](https://etherscan.io/address/0x239Ee6ab18fB06370ad53Ce05097B32208fB0a30) (impl) - Ejector: [`0xe181A377A2d2BDE9A83f1474BC3DB7A412de091E`](https://etherscan.io/address/0xe181A377A2d2BDE9A83f1474BC3DB7A412de091E) - ExitPenalties: [`0x004aFb7DAA7dEA20EbAaB75c9F4892C879FaCCe0`](https://etherscan.io/address/0x004aFb7DAA7dEA20EbAaB75c9F4892C879FaCCe0) (proxy) - ExitPenalties: [`0x3766ABbA4635EE0fC3E6A7EB3EF169fe930df9c2`](https://etherscan.io/address/0x3766ABbA4635EE0fC3E6A7EB3EF169fe930df9c2) (impl) - Factories: - CuratedGateFactory: [`0xDdE99d63b352A665d04339D4792E6852Ce89d1B7`](https://etherscan.io/address/0xDdE99d63b352A665d04339D4792E6852Ce89d1B7) - External libraries: - AssetRecovererLib: [`0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902`](https://etherscan.io/address/0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902) - BondCurvesLib: [`0xC4511d09639e5E174506083443da230D39196323`](https://etherscan.io/address/0xC4511d09639e5E174506083443da230D39196323) - GeneralPenalty: [`0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4`](https://etherscan.io/address/0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4) - NOAddresses: [`0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3`](https://etherscan.io/address/0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3) - NodeOperatorOps: [`0xDD42EE5D54A1822021782F3F455bb99fBC19499A`](https://etherscan.io/address/0xDD42EE5D54A1822021782F3F455bb99fBC19499A) - StakeTracker: [`0xbb6E4Db18182d45038F91B9F1195291c206fd8d2`](https://etherscan.io/address/0xbb6E4Db18182d45038F91B9F1195291c206fd8d2) - WithdrawnValidatorLib: [`0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A`](https://etherscan.io/address/0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A) - CuratedDepositAllocator: [`0xa4fCD4dDa0e4a847142E3592C97c77d8B9B3Cf5F`](https://etherscan.io/address/0xa4fCD4dDa0e4a847142E3592C97c77d8B9B3Cf5F) ## ๐Ÿ’ง Liquidity Pools {#liquidity-pools} - Curve [stETH/ETH](https://curve.fi/steth) pool: [`0xDC24316b9AE028F1497c275EB9192a3Ea0f67022`](https://etherscan.io/address/0xDC24316b9AE028F1497c275EB9192a3Ea0f67022) - Curve concentrated [stETH/wETH](https://curve.fi/factory/117) pool: - Pool contract: [`0x828b154032950C8ff7CF8085D841723Db2696056`](https://etherscan.io/address/0x828b154032950C8ff7CF8085D841723Db2696056) - Gauge contract: [`0xF668E6D326945d499e5B35E7CD2E82aCFbcFE6f0`](https://etherscan.io/address/0xF668E6D326945d499e5B35E7CD2E82aCFbcFE6f0) - Balancer wstETH/WETH pool: [`0x32296969Ef14EB0c6d29669C550D4a0449130230`](https://etherscan.io/address/0x32296969Ef14EB0c6d29669C550D4a0449130230) - Pool id: `0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080` - 1inch stETH/DAI pool: [`0xC1A900Ae76dB21dC5aa8E418Ac0F4E888A4C7431`](https://etherscan.io/address/0xC1A900Ae76dB21dC5aa8E418Ac0F4E888A4C7431) - Sushi wstETH/DAI pool: [`0xc5578194D457dcce3f272538D1ad52c68d1CE849`](https://etherscan.io/address/0xc5578194D457dcce3f272538D1ad52c68d1CE849) ## ๐Ÿ“ˆ Price Feeds {#price-feeds} :::note See [integration guide](/guides/lido-tokens-integration-guide.md#integration-utilities-rate-and-price-feeds) for the rate and price feeds recommended approaches. ::: - Mainnet price feeds - Chainlink wstETH/USD Price Feed: [`0x8b6851156023f4f5a66f68bea80851c3d905ac93`](https://etherscan.io/address/0x8b6851156023f4f5a66f68bea80851c3d905ac93) - Multichain wstETH/stETH rate feeds - Chainlink wstETH/stETH exchange rate on Base: [`0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061`](https://basescan.org/address/0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061) (proxy) - Chainlink wstETH/stETH exchange rate on Arbitrum: [`0xB1552C5e96B312d0Bf8b554186F846C40614a540`](https://arbiscan.io/address/0xb1552c5e96b312d0bf8b554186f846c40614a540) (proxy) - Chainlink wstETH/stETH exchange rate on Optimism: [`0xe59EBa0D492cA53C6f46015EEa00517F2707dc77`](https://optimistic.etherscan.io/address/0xe59eba0d492ca53c6f46015eea00517f2707dc77) (proxy) - Chainlink wstETH/stETH exchange rate on Scroll: [`0xE61Da4C909F7d86797a0D06Db63c34f76c9bCBDC`](https://scrollscan.com/address/0xE61Da4C909F7d86797a0D06Db63c34f76c9bCBDC) (proxy) - Chainlink wstETH/stETH exchange rate on zkSync: [`0x24a0C9404101A8d7497676BE12F10aEa356bAC28`](https://explorer.zksync.io/address/0x24a0C9404101A8d7497676BE12F10aEa356bAC28) (proxy) - Chainlink wstETH/stETH exchange rate on Linea: [`0x3C8A95F2264bB3b52156c766b738357008d87cB7`](https://lineascan.build/address/0x3C8A95F2264bB3b52156c766b738357008d87cB7) (proxy) - Chainlink wstETH/stETH exchange rate on BNB: [`0x4c75d01cfa4D998770b399246400a6dc40FB9645`](https://bscscan.com/address/0x4c75d01cfa4D998770b399246400a6dc40FB9645) (proxy) - Multichain PriceOracle wrappers (immutable adapters over the Chainlink feeds above; used e.g. by CCIP Direct Staking) - PriceOracle on Arbitrum: [`0x328de900860816d29D1367F6903a24D8ed40C997`](https://arbiscan.io/address/0x328de900860816d29D1367F6903a24D8ed40C997) - PriceOracle on Optimism: [`0x301cBCDA894c932E9EDa3Cf8878f78304e69E367`](https://optimistic.etherscan.io/address/0x301cBCDA894c932E9EDa3Cf8878f78304e69E367) - PriceOracle on Base: [`0x301cBCDA894c932E9EDa3Cf8878f78304e69E367`](https://basescan.org/address/0x301cBCDA894c932E9EDa3Cf8878f78304e69E367) - PriceOracle on Linea: [`0x301cBCDA894c932E9EDa3Cf8878f78304e69E367`](https://lineascan.build/address/0x301cBCDA894c932E9EDa3Cf8878f78304e69E367) ## ๐ŸŽ Reward Programs {#reward-programs} - Early Stakers Airdrop: [`0x4b3EDb22952Fb4A70140E39FB1adD05A6B49622B`](https://etherscan.io/address/0x4b3EDb22952Fb4A70140E39FB1adD05A6B49622B) - Curve Liquidity Farming: - Manager Contract: [`0x753D5167C31fBEB5b49624314d74A957Eb271709`](https://etherscan.io/address/0x753D5167C31fBEB5b49624314d74A957Eb271709) - Reward Contract: [`0x99ac10631f69c753ddb595d074422a0922d9056b`](https://etherscan.io/address/0x99ac10631f69c753ddb595d074422a0922d9056b) - Pool Contract: [`0xDC24316b9AE028F1497c275EB9192a3Ea0f67022`](https://etherscan.io/address/0xDC24316b9AE028F1497c275EB9192a3Ea0f67022) - Balancer LP rewards v3: - Manager Contract: [`0x86F6c353A0965eB069cD7f4f91C1aFEf8C725551`](https://etherscan.io/address/0x86F6c353A0965eB069cD7f4f91C1aFEf8C725551) - Arbitrum Curve rewards manager: - Manager Contract: [`0xC20129f1dd4DFeD023a6d6A8de9d54A7b61af5CC`](https://arbiscan.io/address/0xC20129f1dd4DFeD023a6d6A8de9d54A7b61af5CC) - Optimism Curve rewards manager: - Manager Contract: [`0xD420d6C8aA81c087829A64Ce59936b7C1176A81a`](https://optimistic.etherscan.io/address/0xD420d6C8aA81c087829A64Ce59936b7C1176A81a) ## ๐Ÿ”— Legacy Aave V2 Integration {#aave-v2-integration} :::warning The Aave V2 market is being deprecated. Do not use these contracts for new integrations. See the [Aave V2 integration notice](/integrations/aave) for the official legacy position-management path. ::: - AStETH: [`0x1982b2F5814301d4e9a8b0201555376e62F82428`](https://etherscan.io/address/0x1982b2F5814301d4e9a8b0201555376e62F82428) (proxy) - AStETH: [`0xbd233D4ffdAA9B7d1d3E6b18CCcb8D091142893a`](https://etherscan.io/address/0xbd233D4ffdAA9B7d1d3E6b18CCcb8D091142893a) (impl) - StableDebtStETH: [`0x66457616dd8489df5d0afd8678f4a260088aaf55`](https://etherscan.io/address/0x66457616dd8489df5d0afd8678f4a260088aaf55) (proxy) - StableDebtStETH: [`0x8180949ac41EF18e844ff8dafE604a195d86Aea9`](https://etherscan.io/address/0x8180949ac41ef18e844ff8dafe604a195d86aea9) (impl) - VariableDebtStETH: [`0xa9deac9f00dc4310c35603fcd9d34d1a750f81db`](https://etherscan.io/address/0xa9deac9f00dc4310c35603fcd9d34d1a750f81db) (proxy) - VariableDebtStETH: [`0xDe2c414b671d2DB93617D1592f0490c13674de24`](https://etherscan.io/address/0xde2c414b671d2db93617d1592f0490c13674de24) (impl) - DefaultReserveInterestRateStrategy: [`0xff04ed5f7a6C3a0F1e5Ea20617F8C6f513D5A77c`](https://etherscan.io/address/0xff04ed5f7a6C3a0F1e5Ea20617F8C6f513D5A77c) ## โš™๏ธ DAO Ops Contracts & Addresses {#dao-ops-contracts--addresses} - Tokens recoverer for Manager contracts ([Reward Programs](#reward-programs)): [`0x1bdfFe0EBef3FEAdF2723D3330727D73f538959C`](https://etherscan.io/address/0x1bdfFe0EBef3FEAdF2723D3330727D73f538959C). - Token Reward Program (TRP) VestingEscrowFactory: [`0xDA1DF6442aFD2EC36aBEa91029794B9b2156ADD0`](https://etherscan.io/address/0xDA1DF6442aFD2EC36aBEa91029794B9b2156ADD0) ## ๐Ÿค– Bots {#bots} - \[[proposed to remove](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/24)\] Depositor bot: [`0xF82aC5937A20dC862F9bc0668779031E06000f17`](https://etherscan.io/address/0xF82aC5937A20dC862F9bc0668779031E06000f17) - \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/24)\] Depositor bot: [`0x6Aa249bA53A3abcaC52F91146583B3eE2Ee4C7F5`](https://etherscan.io/address/0x6Aa249bA53A3abcaC52F91146583B3eE2Ee4C7F5) ## ๐Ÿชจ Lido Stonks Contracts {#lido-stonks-contracts} - STETHโ†’DAI [`0x3e2D251275A92a8169A3B17A2C49016e2de492a7`](https://etherscan.io/address/0x3e2D251275A92a8169A3B17A2C49016e2de492a7) - STETHโ†’USDC [`0xf4F6A03E3dbf0aA22083be80fDD340943d275Ea5`](https://etherscan.io/address/0xf4F6A03E3dbf0aA22083be80fDD340943d275Ea5) - STETHโ†’USDT [`0x7C2a1E25cA6D778eCaEBC8549371062487846aAF`](https://etherscan.io/address/0x7C2a1E25cA6D778eCaEBC8549371062487846aAF) - DAIโ†’USDC [`0x79f5E20996abE9f6a48AF6f9b13f1E55AED6f06D`](https://etherscan.io/address/0x79f5E20996abE9f6a48AF6f9b13f1E55AED6f06D) - DAIโ†’USDT [`0x8Ba6D367D15Ebc52f3eBBdb4a8710948C0918d42`](https://etherscan.io/address/0x8Ba6D367D15Ebc52f3eBBdb4a8710948C0918d42) - USDTโ†’USDC [`0x281e6BB6F26A94250aCEb24396a8E4190726C97e`](https://etherscan.io/address/0x281e6BB6F26A94250aCEb24396a8E4190726C97e) - USDTโ†’DAI [`0x64B6aF9A108dCdF470E48e4c0147127F26221A7C`](https://etherscan.io/address/0x64B6aF9A108dCdF470E48e4c0147127F26221A7C) - USDCโ†’USDT [`0x278f7B6CBB3Cc37374e6a40bDFEBfff08f65A5C7`](https://etherscan.io/address/0x278f7B6CBB3Cc37374e6a40bDFEBfff08f65A5C7) - USDCโ†’DAI [`0x2B5a3944A654439379B206DE999639508bA2e850`](https://etherscan.io/address/0x2B5a3944A654439379B206DE999639508bA2e850) ## ๐Ÿชบ Lido NEST Contracts {#lido-nest-contracts} - OracleRouter [`0x79ef3a538200Fe4981D67E7e886bfb36D4Cb5a31`](https://etherscan.io/address/0x79ef3a538200Fe4981D67E7e886bfb36D4Cb5a31) - AmountConverter (ETH-anchored) [`0x70dA04C5D0f325F5AF1426dE6672BF2424B4593d`](https://etherscan.io/address/0x70dA04C5D0f325F5AF1426dE6672BF2424B4593d) - StonksFactory [`0x632C0CCDca849eeD780FC685BBa9AbC3c7407Cb2`](https://etherscan.io/address/0x632C0CCDca849eeD780FC685BBa9AbC3c7407Cb2) - Order (sample) [`0x2569633AdB492ca9327cb7433277aa7c68D69e28`](https://etherscan.io/address/0x2569633AdB492ca9327cb7433277aa7c68D69e28) - StakingRevenueSource [`0x6220212a33a87Ed7Cc386B67eB2c393974F28C38`](https://etherscan.io/address/0x6220212a33a87Ed7Cc386B67eB2c393974F28C38) - BuybackExecutor [`0x6c213ca5A10Cc26548C742229569B4AeD2A9C9B7`](https://etherscan.io/address/0x6c213ca5A10Cc26548C742229569B4AeD2A9C9B7) - BuybackAllocator [`0xAA568141c051f2D1132b110f8391F18D48E8D889`](https://etherscan.io/address/0xAA568141c051f2D1132b110f8391F18D48E8D889) - Stonks (LP mode) [`0x8c595aA4AEc6F42B9e7D77F83179768D37CE3042`](https://etherscan.io/address/0x8c595aA4AEc6F42B9e7D77F83179768D37CE3042) - Stonks (Treasury mode) [`0xb368586CB980895E51e1D82102E63b3F69d3F151`](https://etherscan.io/address/0xb368586CB980895E51e1D82102E63b3F69d3F151) ## โšก Easy Track {#easy-track} - EasyTrack: [`0xF0211b7660680B49De1A7E9f25C65660F0a13Fea`](https://etherscan.io/address/0xF0211b7660680B49De1A7E9f25C65660F0a13Fea) - EVMScriptExecutor: [`0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977`](https://etherscan.io/address/0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977) ### โš™๏ธ Easy Track Factories for Core Protocol {#easy-track-factories-for-core-protocol} - \[[proposed](https://research.lido.fi/t/proposal-add-easy-track-factory-for-deposit-reserve-target-management-by-cmc/11827/6)\] SetDepositsReserveTarget: [`0x62E9Dc68BDCBC46362f40e0bb9c154C9a42E62b0`](https://etherscan.io/address/0x62E9Dc68BDCBC46362f40e0bb9c154C9a42E62b0) ### ๐Ÿงฉ Easy Track Factories for Staking Modules {#easy-track-factories-for-staking-modules} - **Curated Node Operators staking module** (registry: [`0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5`](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5)) - IncreaseNodeOperatorStakingLimit: [`0xFeBd8FAC16De88206d4b18764e826AF38546AfE0`](https://etherscan.io/address/0xFeBd8FAC16De88206d4b18764e826AF38546AfE0) - CuratedSubmitExitRequestHashes: [`0x4F716AD3Cc7A3A5cdA2359e5B2c84335c171dCde`](https://etherscan.io/address/0x4F716AD3Cc7A3A5cdA2359e5B2c84335c171dCde) - **Simple DVT staking module** (registry: [`0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433`](https://etherscan.io/address/0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433), committee ms [`0x08637515E85A4633E23dfc7861e2A9f53af640f7`](https://app.safe.global/settings/setup?safe=eth:0x08637515E85A4633E23dfc7861e2A9f53af640f7)) - AddNodeOperators: [`0xcAa3AF7460E83E665EEFeC73a7a542E5005C9639`](https://etherscan.io/address/0xcAa3AF7460E83E665EEFeC73a7a542E5005C9639) - ActivateNodeOperators: [`0xCBb418F6f9BFd3525CE6aADe8F74ECFEfe2DB5C8`](https://etherscan.io/address/0xCBb418F6f9BFd3525CE6aADe8F74ECFEfe2DB5C8) - DeactivateNodeOperators: [`0x8B82C1546D47330335a48406cc3a50Da732672E7`](https://etherscan.io/address/0x8B82C1546D47330335a48406cc3a50Da732672E7) - SetVettedValidatorsLimits: [`0xD75778b855886Fc5e1eA7D6bFADA9EB68b35C19D`](https://etherscan.io/address/0xD75778b855886Fc5e1eA7D6bFADA9EB68b35C19D) - SetNodeOperatorNames: [`0x7d509BFF310d9460b1F613e4e40d342201a83Ae4`](https://etherscan.io/address/0x7d509BFF310d9460b1F613e4e40d342201a83Ae4) - SetNodeOperatorRewardAddresses: [`0x589e298964b9181D9938B84bB034C3BB9024E2C0`](https://etherscan.io/address/0x589e298964b9181D9938B84bB034C3BB9024E2C0) - UpdateTargetValidatorLimits: [`0x161a4552a625844c822954c5acbac928ee0f399b`](https://etherscan.io/address/0x161a4552a625844c822954c5acbac928ee0f399b) - ChangeNodeOperatorManager: [`0xE31A0599A6772BCf9b2bFc9e25cf941e793c9a7D`](https://etherscan.io/address/0xE31A0599A6772BCf9b2bFc9e25cf941e793c9a7D) - SDVTSubmitExitRequestHashes: [`0x58A59dDC6Aea9b1D5743D024E15DfA4badB56E37`](https://etherscan.io/address/0x58A59dDC6Aea9b1D5743D024E15DfA4badB56E37) - **Community Staking Module** (module: [`0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F`](https://etherscan.io/address/0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F), committee ms [`0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f`](https://app.safe.global/settings/setup?safe=eth:0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f)) - SetMerkleGateTree: [`0xf3ec30B86c3dC1b8a1C754D885F9bE3160e15B4c`](https://etherscan.io/address/0xf3ec30B86c3dC1b8a1C754D885F9bE3160e15B4c) - ReportWithdrawalsForSlashedValidators: [`0xE330516a03bDdEBA4209b5591112f1aa3dd90F0A`](https://etherscan.io/address/0xE330516a03bDdEBA4209b5591112f1aa3dd90F0A) - SettleGeneralDelayedPenalty: [`0xB71755bE764abB4Ce26cb4dADf056Be57fB8880F`](https://etherscan.io/address/0xB71755bE764abB4Ce26cb4dADf056Be57fB8880F) - UpdateStakingModuleShareLimits: [`0xde3e46E3129fA4e4e3f66c9024B0A3Ad509b27a1`](https://etherscan.io/address/0xde3e46E3129fA4e4e3f66c9024B0A3Ad509b27a1) - **Curated Module v2** (module: [`0xDa5F930cE326EB5205085D66c72A4E79d60cB8C1`](https://etherscan.io/address/0xDa5F930cE326EB5205085D66c72A4E79d60cB8C1), committee ms [`0x2570e0b22AD904501dfB0d49575991ACB801dD91`](https://app.safe.global/settings/setup?safe=eth:0x2570e0b22AD904501dfB0d49575991ACB801dD91)) - SetMerkleGateTree: [`0xa121667D1780a1D54EAEd67AE17ee13d0f872D60`](https://etherscan.io/address/0xa121667D1780a1D54EAEd67AE17ee13d0f872D60) - ReportWithdrawalsForSlashedValidators: [`0x71862Abd99819597670007bb992A7a7562fE50f2`](https://etherscan.io/address/0x71862Abd99819597670007bb992A7a7562fE50f2) - SettleGeneralDelayedPenalty: [`0xfffEFC16231eDC6Dc9C93e364ff4D4E3f787f416`](https://etherscan.io/address/0xfffEFC16231eDC6Dc9C93e364ff4D4E3f787f416) - CreateOrUpdateOperatorGroup: [`0x2fC78638b77381e9D040163Bd6EB1cac967bDBdF`](https://etherscan.io/address/0x2fC78638b77381e9D040163Bd6EB1cac967bDBdF) - AllowConsolidationPair: [`0x29e23B1EF0c9fffAc8330F9abaCebDDD827E4b5C`](https://etherscan.io/address/0x29e23B1EF0c9fffAc8330F9abaCebDDD827E4b5C) ### ๐Ÿ’ฐ Easy Track Factories for Token Transfers {#easy-track-factories-for-token-transfers} - **LOL (ex.reWARDS) stETH** (committee ms [`0x87D93d9B2C672bf9c9642d853a8682546a5012B5`](https://app.safe.global/settings/setup?safe=eth:0x87D93d9B2C672bf9c9642d853a8682546a5012B5)) - AllowedRecipientsRegistry: [`0x48c4929630099b217136b64089E8543dB0E5163a`](https://etherscan.io/address/0x48c4929630099b217136b64089E8543dB0E5163a) - AddAllowedRecipient: [`0x935cb3366Faf2cFC415B2099d1F974Fd27202b77`](https://etherscan.io/address/0x935cb3366Faf2cFC415B2099d1F974Fd27202b77) - RemoveAllowedRecipient: [`0x22010d1747CaFc370b1f1FBBa61022A313c5693b`](https://etherscan.io/address/0x22010d1747CaFc370b1f1FBBa61022A313c5693b) - TopUpAllowedRecipients: [`0x1F2b79FE297B7098875930bBA6dd17068103897E`](https://etherscan.io/address/0x1F2b79FE297B7098875930bBA6dd17068103897E) - **LOL (ex.reWARDS) stablecoins** (committee ms [`0x87D93d9B2C672bf9c9642d853a8682546a5012B5`](https://app.safe.global/settings/setup?safe=eth:0x87D93d9B2C672bf9c9642d853a8682546a5012B5)) - AllowedRecipientsRegistry: [`0x8d8b35cA51e7808098afF4918C21Ce428c943F89`](https://etherscan.io/address/0x8d8b35cA51e7808098afF4918C21Ce428c943F89) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - AddAllowedRecipient: [`0xe24230619e9218C1eed3de3489a22f6BC3ce18FF`](https://etherscan.io/address/0xe24230619e9218C1eed3de3489a22f6BC3ce18FF) - RemoveAllowedRecipient: [`0xF4d5D97C85eD18f77F99B57f55E9E11d52992632`](https://etherscan.io/address/0xF4d5D97C85eD18f77F99B57f55E9E11d52992632) - TopUpAllowedRecipients: [`0xc72d4C3e86b681D7c9EE306D41193C64D709C303`](https://etherscan.io/address/0xc72d4C3e86b681D7c9EE306D41193C64D709C303) - **Rewards Share stETH** (committee ms [`0xe2A682A9722354D825d1BbDF372cC86B2ea82c8C`](https://app.safe.global/settings/setup?safe=eth:0xe2A682A9722354D825d1BbDF372cC86B2ea82c8C)) - AllowedRecipientsRegistry: [`0xdc7300622948a7AdaF339783F6991F9cdDD79776`](https://etherscan.io/address/0xdc7300622948a7AdaF339783F6991F9cdDD79776) - AddAllowedRecipient: [`0x1F809D2cb72a5Ab13778811742050eDa876129b6`](https://etherscan.io/address/0x1F809D2cb72a5Ab13778811742050eDa876129b6) - RemoveAllowedRecipient: [`0xd30Dc38EdEfc21875257e8A3123503075226E14B`](https://etherscan.io/address/0xd30Dc38EdEfc21875257e8A3123503075226E14B) - TopUpAllowedRecipients: [`0xbD08f9D6BF1D25Cc7407E4855dF1d46C2043B3Ea`](https://etherscan.io/address/0xbD08f9D6BF1D25Cc7407E4855dF1d46C2043B3Ea) - **LEGO LDO** (committee ms [`0x12a43b049A7D330cB8aEAB5113032D18AE9a9030`](https://app.safe.global/settings/setup?safe=eth:0x12a43b049A7D330cB8aEAB5113032D18AE9a9030)) - AllowedRecipientsRegistry: [`0x97615f72c3428A393d65A84A3ea6BBD9ad6C0D74`](https://etherscan.io/address/0x97615f72c3428A393d65A84A3ea6BBD9ad6C0D74) - TopUpAllowedRecipients: [`0x00caAeF11EC545B192f16313F53912E453c91458`](https://etherscan.io/address/0x00caAeF11EC545B192f16313F53912E453c91458) - **LEGO stablecoins** (committee ms [`0x12a43b049A7D330cB8aEAB5113032D18AE9a9030`](https://app.safe.global/settings/setup?safe=eth:0x12a43b049A7D330cB8aEAB5113032D18AE9a9030)) - AllowedRecipientsRegistry: [`0xb0FE4D300334461523D9d61AaD90D0494e1Abb43`](https://etherscan.io/address/0xb0FE4D300334461523D9d61AaD90D0494e1Abb43) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0x6AB39a8Be67D9305799c3F8FdFc95Caf3150d17c`](https://etherscan.io/address/0x6AB39a8Be67D9305799c3F8FdFc95Caf3150d17c) - **TRP LDO** (committee ms [`0x834560F580764Bc2e0B16925F8bF229bb00cB759`](https://app.safe.global/settings/setup?safe=eth:0x834560F580764Bc2e0B16925F8bF229bb00cB759)) - AllowedRecipientsRegistry: [`0x231Ac69A1A37649C6B06a71Ab32DdD92158C80b8`](https://etherscan.io/address/0x231Ac69A1A37649C6B06a71Ab32DdD92158C80b8) - TopUpAllowedRecipients: [`0xBd2b6dC189EefD51B273F5cb2d99BA1ce565fb8C`](https://etherscan.io/address/0xBd2b6dC189EefD51B273F5cb2d99BA1ce565fb8C) - **Gas Supply stETH** (committee ms [`0x5181d5D56Af4f823b96FE05f062D7a09761a5a53`](https://app.safe.global/settings/setup?safe=eth:0x5181d5D56Af4f823b96FE05f062D7a09761a5a53)) - AllowedRecipientsRegistry: [`0x49d1363016aA899bba09ae972a1BF200dDf8C55F`](https://etherscan.io/address/0x49d1363016aA899bba09ae972a1BF200dDf8C55F) - AddAllowedRecipient: [`0x48c135Ff690C2Aa7F5B11C539104B5855A4f9252`](https://etherscan.io/address/0x48c135Ff690C2Aa7F5B11C539104B5855A4f9252) - RemoveAllowedRecipient: [`0x7E8eFfAb3083fB26aCE6832bFcA4C377905F97d7`](https://etherscan.io/address/0x7E8eFfAb3083fB26aCE6832bFcA4C377905F97d7) - TopUpAllowedRecipients: [`0x200dA0b6a9905A377CF8D469664C65dB267009d1`](https://etherscan.io/address/0x200dA0b6a9905A377CF8D469664C65dB267009d1) - **Alliance Ops stablecoins** (committee ms [`0x606f77BF3dd6Ed9790D9771C7003f269a385D942`](https://app.safe.global/settings/setup?safe=eth:0x606f77BF3dd6Ed9790D9771C7003f269a385D942)) - AllowedRecipientsRegistry: [`0x3B525F4c059F246Ca4aa995D21087204F30c9E2F`](https://etherscan.io/address/0x3B525F4c059F246Ca4aa995D21087204F30c9E2F) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0xe5656eEe7eeD02bdE009d77C88247BC8271e26Eb`](https://etherscan.io/address/0xe5656eEe7eeD02bdE009d77C88247BC8271e26Eb) - **Stonks stETH** (committee ms [`0xa02FC823cCE0D016bD7e17ac684c9abAb2d6D647`](https://app.safe.global/settings/setup?safe=eth:0xa02FC823cCE0D016bD7e17ac684c9abAb2d6D647)) - AllowedRecipientsRegistry: [`0x1a7cFA9EFB4D5BfFDE87B0FaEb1fC65d653868C0`](https://etherscan.io/address/0x1a7cFA9EFB4D5BfFDE87B0FaEb1fC65d653868C0) - TopUpAllowedRecipients: [`0x6e04aED774B7c89BB43721AcDD7D03C872a51B69`](https://etherscan.io/address/0x6e04aED774B7c89BB43721AcDD7D03C872a51B69) - **Stonks stablecoins** (committee ms [`0xa02FC823cCE0D016bD7e17ac684c9abAb2d6D647`](https://app.safe.global/settings/setup?safe=eth:0xa02FC823cCE0D016bD7e17ac684c9abAb2d6D647)) - AllowedRecipientsRegistry: [`0x3f0534CCcFb952470775C516DC2eff8396B8A368`](https://etherscan.io/address/0x3f0534CCcFb952470775C516DC2eff8396B8A368) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0x0d2aefA542aFa8d9D1Ec35376068B88042FEF5f6`](https://etherscan.io/address/0x0d2aefA542aFa8d9D1Ec35376068B88042FEF5f6) - **Ecosystem BORG Foundation operational funds stablecoins** (committee ms [`0x55897893c19e4B0c52731a3b7C689eC417005Ad6`](https://app.safe.global/settings/setup?safe=eth:0x55897893c19e4B0c52731a3b7C689eC417005Ad6)) - AllowedRecipientsRegistry: [`0xDAdC4C36cD8F468A398C25d0D8aaf6A928B47Ab4`](https://etherscan.io/address/0xDAdC4C36cD8F468A398C25d0D8aaf6A928B47Ab4) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0xf2476f967C826722F5505eDfc4b2561A34033477`](https://etherscan.io/address/0xf2476f967C826722F5505eDfc4b2561A34033477) - **Labs BORG Foundation operational funds stablecoins** (committee ms [`0x95B521B4F55a447DB89f6a27f951713fC2035f3F`](https://app.safe.global/settings/setup?safe=eth:0x95B521B4F55a447DB89f6a27f951713fC2035f3F)) - AllowedRecipientsRegistry: [`0x68267f3D310E9f0FF53a37c141c90B738E1133c2`](https://etherscan.io/address/0x68267f3D310E9f0FF53a37c141c90B738E1133c2) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0xE1f6BaBb445F809B97e3505Ea91749461050F780`](https://etherscan.io/address/0xE1f6BaBb445F809B97e3505Ea91749461050F780) - **Tooling contracts:** - AllowedRecipientsBuilder (single token): [`0x958e0D946D014F377421a53AB5f9180d4485e63B`](https://etherscan.io/address/0x958e0D946D014F377421a53AB5f9180d4485e63B) - AllowedRecipientsFactory (single token): [`0x83E976758B7AB1bb676A4fEA073Fa0E2A807642B`](https://etherscan.io/address/0x83E976758B7AB1bb676A4fEA073Fa0E2A807642B) - AllowedRecipientsBuilder (multi token): [`0x334D6eDc13F63728b39e6A6D04A7Bbd5D6A9B9FF`](https://etherscan.io/address/0x334D6eDc13F63728b39e6A6D04A7Bbd5D6A9B9FF) - AllowedRecipientsFactory (multi token): [`0xEe60C6ebC91237d334230b12263E26EE3b480ec4`](https://etherscan.io/address/0xEe60C6ebC91237d334230b12263E26EE3b480ec4) - BokkyPooBah's DateTime Library: [`0x75100bd564415731b5936a4a94d0dc29dde5db3c`](https://etherscan.io/address/0x75100bd564415731b5936a4a94d0dc29dde5db3c) ### ๐Ÿค– Easy Track Factories for MEV-Boost Relay Allowed List management {#easy-track-factories-for-mev-boost-relay-allowed-list-management} - **MEV-Boost Relay Allowed List** (committee ms [`0x98be4a407Bff0c125e25fBE9Eb1165504349c37d`](https://app.safe.global/settings/setup?safe=eth:0x98be4a407Bff0c125e25fBE9Eb1165504349c37d)) - AddMEVBoostRelays: [`0x00A3D6260f70b1660c8646Ef25D0820EFFd7bE60`](https://etherscan.io/address/0x00A3D6260f70b1660c8646Ef25D0820EFFd7bE60) - RemoveMEVBoostRelays: [`0x9721c0f77E3Ea40eD592B9DCf3032DaF269c0306`](https://etherscan.io/address/0x9721c0f77E3Ea40eD592B9DCf3032DaF269c0306) - EditMEVBoostRelay: [`0x6b7863f2c7dEE99D3b744fDAEDbEB1aeCC025535`](https://etherscan.io/address/0x6b7863f2c7dEE99D3b744fDAEDbEB1aeCC025535) ### ๐Ÿ”จ Easy Track Factories for stVaults Management {#easy-track-factories-for-stvaults-management} - **Operator Grid:** (trusted caller is stVaults Committee ms [`0x18A1065c81b0Cc356F1b1C843ddd5E14e4AefffF`](https://app.safe.global/settings/setup?safe=eth:0x18A1065c81b0Cc356F1b1C843ddd5E14e4AefffF)) - Register Groups: [`0x17305dB55c908e84C58BbDCa57258A7D1f7eEa7c`](https://etherscan.io/address/0x17305dB55c908e84C58BbDCa57258A7D1f7eEa7c) - Update Groups Share Limit: [`0xf23559De8ab37fF7a154384B0822dA867Cfa7Eac`](https://etherscan.io/address/0xf23559De8ab37fF7a154384B0822dA867Cfa7Eac) - Register Tiers: [`0x6b535F441F95046562406F4E2518D9AD7Db2dc0D`](https://etherscan.io/address/0x6b535F441F95046562406F4E2518D9AD7Db2dc0D) - Alter Tiers: [`0x37d9B09EDA477a84E3913fCB4d032EFb0BF9B62E`](https://etherscan.io/address/0x37d9B09EDA477a84E3913fCB4d032EFb0BF9B62E) - Set Jail Status: [`0x6a4f33F05E7412A11100353724Bb6a152Cf0D305`](https://etherscan.io/address/0x6a4f33F05E7412A11100353724Bb6a152Cf0D305) - Update Vaults Fees: [`0xDfA0bc38113B6d53c2881573FD764CEEFf468610`](https://etherscan.io/address/0xDfA0bc38113B6d53c2881573FD764CEEFf468610) - **Vault Hub:** (trusted caller is stVaults Committee ms [`0x18A1065c81b0Cc356F1b1C843ddd5E14e4AefffF`](https://app.safe.global/settings/setup?safe=eth:0x18A1065c81b0Cc356F1b1C843ddd5E14e4AefffF)) - Force Validator Exits: [`0x6F5c0A5a824773E8f8285bC5aA59ea0Aab2A6400`](https://etherscan.io/address/0x6F5c0A5a824773E8f8285bC5aA59ea0Aab2A6400) - Socialize Bad Debt: [`0xaf35A63a4114B7481589fDD9FDB3e35Fd65fAed7`](https://etherscan.io/address/0xaf35A63a4114B7481589fDD9FDB3e35Fd65fAed7) - VaultsAdapter: [`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27) ## ๐Ÿ” Lido DAO Multisigs {#lido-dao-multisigs} ### ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Committees {#committees} - LEGO Committee: [`0x12a43b049A7D330cB8aEAB5113032D18AE9a9030`](https://app.safe.global/settings/setup?safe=eth:0x12a43b049A7D330cB8aEAB5113032D18AE9a9030) - Rewards Share Committee: [`0xe2A682A9722354D825d1BbDF372cC86B2ea82c8C`](https://app.safe.global/settings/setup?safe=eth:0xe2A682A9722354D825d1BbDF372cC86B2ea82c8C) - Relay Maintenance Committee: [`0x98be4a407Bff0c125e25fBE9Eb1165504349c37d`](https://app.safe.global/settings/setup?safe=eth:0x98be4a407Bff0c125e25fBE9Eb1165504349c37d) - Token Reward Program (TRP) Committee: [`0x834560F580764Bc2e0B16925F8bF229bb00cB759`](https://app.safe.global/settings/setup?safe=eth:0x834560F580764Bc2e0B16925F8bF229bb00cB759) - Treasury Management Committee: [`0xa02FC823cCE0D016bD7e17ac684c9abAb2d6D647`](https://app.safe.global/settings/setup?safe=eth:0xa02FC823cCE0D016bD7e17ac684c9abAb2d6D647) - Delegate Oversight Committee: [`0x13600b9AEE86f8254969918B1E9ae6ea091b8727`](https://app.safe.global/settings/setup?safe=eth:0x13600b9AEE86f8254969918B1E9ae6ea091b8727) - Simple DVT Module Committee: [`0x08637515E85A4633E23dfc7861e2A9f53af640f7`](https://app.safe.global/settings/setup?safe=eth:0x08637515E85A4633E23dfc7861e2A9f53af640f7) - Community Staking Module Committee: [`0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f`](https://app.safe.global/settings/setup?safe=eth:0xC52fC3081123073078698F1EAc2f1Dc7Bd71880f) - Curated Module Committee: [`0x2570e0b22AD904501dfB0d49575991ACB801dD91`](https://app.safe.global/settings/setup?safe=eth:0x2570e0b22AD904501dfB0d49575991ACB801dD91) - Bug Bounty Reserve Multisig: [`0x9Eb81629245C5248A8f4FfCDf11A73E0D0C74071`](https://app.safe.global/settings/setup?safe=eth:0x9Eb81629245C5248A8f4FfCDf11A73E0D0C74071) ### ๐Ÿค Alliance {#alliance} - Lido Alliance BORG Foundation Operational: [`0x606f77BF3dd6Ed9790D9771C7003f269a385D942`](https://app.safe.global/settings/setup?safe=eth:0x606f77BF3dd6Ed9790D9771C7003f269a385D942) - Lido Alliance BORG Allies/Partners: [`0x92ABC000698374B44206148596AcD8a934687E66`](https://app.safe.global/settings/setup?safe=eth:0x92ABC000698374B44206148596AcD8a934687E66) - Lido Alliance BORG Drop: [`neutron1fdjng7sdfrn22xlyl923v5fngyvzhllvjkrewv2e932qf54fs79srz0jfr`](https://daodao.zone/dao/neutron1fdjng7sdfrn22xlyl923v5fngyvzhllvjkrewv2e932qf54fs79srz0jfr) ### ๐Ÿ› ๏ธ Dev Team Multisigs {#dev-team-multisigs} - Gas Supply Committee: [`0x5181d5D56Af4f823b96FE05f062D7a09761a5a53`](https://etherscan.io/address/0x5181d5D56Af4f823b96FE05f062D7a09761a5a53) - Lido Subgraph NFT owner: [`0x14CeF290c79fc84FDDfDf4129Ba335972aAc7F41`](https://etherscan.io/address/0x14CeF290c79fc84FDDfDf4129Ba335972aAc7F41) ### ๐Ÿ›‘ Emergency Brakes Multisigs {#emergency-brakes-multisigs} - CircuitBreaker Committee: [`0x8772E3a2D86B9347A2688f9bc1808A6d8917760C`](https://app.safe.global/settings/setup?safe=eth:0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) - Ethereum: [`0x73b047fe6337183A454c5217241D780a932777bD`](https://app.safe.global/settings/setup?safe=eth:0x73b047fe6337183A454c5217241D780a932777bD) - Optimism: [`0x4Cf8fE0A4c2539F7EFDD2047d8A5D46F14613088`](https://app.safe.global/settings/setup?safe=oeth:0x4Cf8fE0A4c2539F7EFDD2047d8A5D46F14613088) - Arbitrum: [`0xfDCf209A213a0b3C403d543F87E74FCbcA11de34`](https://app.safe.global/settings/setup?safe=arb1:0xfDCf209A213a0b3C403d543F87E74FCbcA11de34) - Base: [`0x0F9A0e7071B7B21bc7a8514DA2cd251bc1FF0725`](https://app.safe.global/settings/setup?safe=base:0x0F9A0e7071B7B21bc7a8514DA2cd251bc1FF0725) - ZKSync: [`0x0D7F0A811978B3B62CbfF4EF6149B5909EAcfE94`](https://app.safe.global/settings/setup?safe=zksync:0x0D7F0A811978B3B62CbfF4EF6149B5909EAcfE94) - Mantle: [`0xa8579D42E34398267dE16e6eeeCdb7ED0EFF953C`](https://multisig.mantle.xyz/settings/setup?safe=mantle:0xa8579D42E34398267dE16e6eeeCdb7ED0EFF953C) - Scroll: [`0xF580753E334687C0d6b88EF563a258f048384Ee6`](https://app.safe.global/settings/setup?safe=scr:0xF580753E334687C0d6b88EF563a258f048384Ee6) - Mode: [`0x244912352A639001ceCFa208cDaa7CB474c9eadE`](https://safe.optimism.io/settings/setup?safe=mode:0x244912352A639001ceCFa208cDaa7CB474c9eadE) - Binance Smart Chain (BSC): [`0xC2b778fCc3FF311Cf1abBF4E53880277bfD14C8f`](https://app.safe.global/settings/setup?safe=bnb:0xC2b778fCc3FF311Cf1abBF4E53880277bfD14C8f) - Zircuit: [`0x9Bff79BF7226cB5C16d0Cca9c1dc60450feE560d`](https://safe.zircuit.com/settings/setup?safe=zircuit-mainnet:0x9Bff79BF7226cB5C16d0Cca9c1dc60450feE560d) - Soneium: [`0x993F92e031B86b229D639463325f9d6a51609b43`](https://safe.optimism.io/settings/setup?safe=soneium:0x993F92e031B86b229D639463325f9d6a51609b43) - Unichain: [`0xac8bc65814Dd0501674f6940aff1a4Ea78Fc20eF`](https://app.safe.global/settings/setup?safe=unichain:0xac8bc65814Dd0501674f6940aff1a4Ea78Fc20eF) - Lisk: [`0x1356C0b19c2531bBf0Dd23E585b7C7f7096EeC39`](https://safe.optimism.io/settings/setup?safe=lisk:0x1356C0b19c2531bBf0Dd23E585b7C7f7096EeC39) - Swellchain: [`0xC2b778fCc3FF311Cf1abBF4E53880277bfD14C8f`](https://safe.optimism.io/settings/setup?safe=swell-l2:0xC2b778fCc3FF311Cf1abBF4E53880277bfD14C8f) ### ๐Ÿ”ฌ Liquidity Observation Lab Multisigs {#liquidity-observation-lab-multisigs} - Liquidity Observation Lab: [`0x87D93d9B2C672bf9c9642d853a8682546a5012B5`](https://app.safe.global/settings/setup?safe=eth:0x87D93d9B2C672bf9c9642d853a8682546a5012B5) (Ethereum) - Liquidity Observation Lab: [`0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61`](https://app.safe.global/settings/setup?safe=oeth:0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61) (Optimism) - Liquidity Observation Lab OP Token Multisig: [`0x91cE2F083d59B832f95f90aA0997168ae051a98A`](https://app.safe.global/settings/setup?safe=oeth:0x91cE2F083d59B832f95f90aA0997168ae051a98A) (Optimism) - Liquidity Observation Lab: [`0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61`](https://app.safe.global/settings/setup?safe=arb1:0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61) (Arbitrum) - Liquidity Observation Lab ARB Token Multisig: [`0x1840c4D81d2C50B603da5391b6A24c1cD62D0B56`](https://app.safe.global/settings/setup?safe=arb1:0x1840c4D81d2C50B603da5391b6A24c1cD62D0B56) (Arbitrum) - Liquidity Observation Lab Arbitrum LTIPP Grant Token Multisig: [`0xD97221065E826167A2cFE3307972c0D42200fDB4`](https://app.safe.global/settings/setup?safe=arb1:0xD97221065E826167A2cFE3307972c0D42200fDB4) (Arbitrum) - Liquidity Observation Lab: [`0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61`](https://app.safe.global/settings/setup?safe=base:0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61) (Base) - Liquidity Observation Lab: [`0x65B05f4fCa066316383b0FE196C76C873a4dFD02`](https://app.safe.global/settings/setup?safe=zksync:0x65B05f4fCa066316383b0FE196C76C873a4dFD02) (zkSync Era) - Liquidity Observation Lab ZK Token Multisig: [`0xf7169E14CDEF99403BE9114c9303887f760B1913`](https://app.safe.global/settings/setup?safe=zksync:0xf7169E14CDEF99403BE9114c9303887f760B1913) (zkSync Era) - Liquidity Observation Lab: [`0x87D93d9B2C672bf9c9642d853a8682546a5012B5`](https://app.safe.global/settings/setup?safe=matic:0x87D93d9B2C672bf9c9642d853a8682546a5012B5) (Polygon) - Liquidity Observation Lab: [`0xDAFc1dcB93dA415604aC6187638F88a8Ff8d77A4`](https://multisig.moonbeam.network/settings/setup?safe=mriver:0xDAFc1dcB93dA415604aC6187638F88a8Ff8d77A4) (Moonriver) - Liquidity Observation Lab: [`0x007132343cA619C5449297507B26c3f85e80D1b1`](https://multisig.moonbeam.network/settings/setup?safe=mbeam:0x007132343cA619C5449297507B26c3f85e80D1b1) (Moonbeam) - Liquidity Observation Lab: [`0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61`](https://app.safe.global/settings/setup?safe=bnb:0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61) (BNB) - Liquidity Observation Lab: [`0xA8ef4Db842D95DE72433a8b5b8FF40CB7C74C1b6`](https://safe.linea.build/settings/setup?safe=linea:0xA8ef4Db842D95DE72433a8b5b8FF40CB7C74C1b6) (Linea) - Liquidity Observation Lab: [`0x6Ef6cd595b775B9752df83C8b1700235b21FE2f6`](https://multisig.mantle.xyz/settings/setup?safe=mantle:0x6Ef6cd595b775B9752df83C8b1700235b21FE2f6) (Mantle) - Liquidity Observation Lab: [`0x7bA516FB4512877C016907D6e70FAE96fbbdf8cD`](https://app.safe.global/settings/setup?safe=scr:0x7bA516FB4512877C016907D6e70FAE96fbbdf8cD) (Scroll) - Liquidity Observation Lab AAVE rewards: `0xC18F11735C6a1941431cCC5BcF13AF0a052A5022` ([Ethereum](https://app.safe.global/settings/setup?safe=eth:0xC18F11735C6a1941431cCC5BcF13AF0a052A5022), [Arbitrum](https://app.safe.global/settings/setup?safe=arb1:0xC18F11735C6a1941431cCC5BcF13AF0a052A5022), [BNB](https://app.safe.global/settings/setup?safe=bnb:0xC18F11735C6a1941431cCC5BcF13AF0a052A5022), [Polygon](https://app.safe.global/settings/setup?safe=matic:0xC18F11735C6a1941431cCC5BcF13AF0a052A5022), [Scroll](https://app.safe.global/settings/setup?safe=scr:0xC18F11735C6a1941431cCC5BcF13AF0a052A5022)) - Liquidity Observation Lab AAVE rewards: `0x4f793e5d1d71dbbcEE34E39A5aD3c6bA5b11e935` ([Base](https://app.safe.global/settings/setup?safe=base:0x4f793e5d1d71dbbcEE34E39A5aD3c6bA5b11e935)) - Liquidity Observation Lab AAVE rewards: `0x75483CE83100890c6bf1718c26052cE44e0F2839` ([Optimism](https://app.safe.global/settings/setup?safe=oeth:0x75483CE83100890c6bf1718c26052cE44e0F2839)) - Liquidity Observation Lab AAVE rewards: `0xADB90Cfb3d5ebbaB8eeE7DA10B4DB215A7d50BeE` ([zksync](https://app.safe.global/settings/setup?safe=zksync:0xADB90Cfb3d5ebbaB8eeE7DA10B4DB215A7d50BeE)) ### ๐Ÿ“ฃ Lido on X {#lido-on-x} - Lido on Polygon: [`0xd65Fa54F8DF43064dfd8dDF223A446fc638800A9`](https://app.safe.global/settings/setup?safe=0xd65Fa54F8DF43064dfd8dDF223A446fc638800A9) ### ๐Ÿ—‚๏ธ Other {#other} - Community Lifeguards Multisig: [`0x6faCCcE132d5C397068807Ca73883d3df198dFF4`](https://app.safe.global/settings/setup?safe=eth:0x6faCCcE132d5C397068807Ca73883d3df198dFF4) ## ๐ŸŒ Lido Multichain {#lido-multichain} ### ๐ŸŒ€ Arbitrum {#arbitrum} ##### ๐Ÿงฑ Ethereum part {#ethereum-part-arbitrum} - L1ERC20TokenGateway: [`0x0F25c1DC2a9922304f2eac71DCa9B07E310e8E5a`](https://etherscan.io/address/0x0F25c1DC2a9922304f2eac71DCa9B07E310e8E5a) (proxy) - L1ERC20TokenGateway: [`0xc4E3ff0b5B106f88Fc64c43031BE8b076ee9F21C`](https://etherscan.io/address/0xc4E3ff0b5B106f88Fc64c43031BE8b076ee9F21C) (impl) ##### ๐ŸŒ€ Arbitrum part {#arbitrum-part} - WstETH ERC20Bridged: [`0x5979D7b546E38E414F7E9822514be443A4800529`](https://arbiscan.io/address/0x5979D7b546E38E414F7E9822514be443A4800529) (proxy) - WstETH ERC20Bridged: [`0x0fBcbaEA96Ce0cF7Ee00A8c19c3ab6f5Dc8E1921`](https://arbiscan.io/address/0x0fBcbaEA96Ce0cF7Ee00A8c19c3ab6f5Dc8E1921) (impl) - L2ERC20TokenGateway: [`0x07D4692291B9E30E326fd31706f686f83f331B82`](https://arbiscan.io/address/0x07D4692291B9E30E326fd31706f686f83f331B82) (proxy) - L2ERC20TokenGateway: [`0xe75886DE20dF66827e321EfdB88726e6Baa4b0A7`](https://arbiscan.io/address/0xe75886DE20dF66827e321EfdB88726e6Baa4b0A7) (impl) - Arbitrum Governance Bridge Executor: [`0x1dcA41859Cd23b526CBe74dA8F48aC96e14B1A29`](https://arbiscan.io/address/0x1dca41859cd23b526cbe74da8f48ac96e14b1a29) ### ๐ŸŒž Optimism {#optimism} #### ๐Ÿงฑ Ethereum part {#ethereum-part-optimism} - TokenRateNotifier: [`0xbe05d12Fd10919F1881125006523452F6aFF791b`](https://etherscan.io/address/0xbe05d12Fd10919F1881125006523452F6aFF791b) - OpStackTokenRatePusher: [`0xd54c1c6413caac3477AC14b2a80D5398E3c32FfE`](https://etherscan.io/address/0xd54c1c6413caac3477AC14b2a80D5398E3c32FfE) - L1LidoTokensBridge: [`0x76943C0D61395d8F2edF9060e1533529cAe05dE6`](https://etherscan.io/address/0x76943C0D61395d8F2edF9060e1533529cAe05dE6) (proxy) - L1LidoTokensBridge: [`0x168Cfea1Ad879d7032B3936eF3b0E90790b6B6D4`](https://etherscan.io/address/0x168Cfea1Ad879d7032B3936eF3b0E90790b6B6D4) (impl) #### ๐ŸŒž Optimism part {#optimism-part} - WstETH ERC20BridgedPermit: [`0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb`](https://optimistic.etherscan.io/address/0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb) (proxy) - WstETH ERC20BridgedPermit: [`0xFe57042De76c8D6B1DF0E9E2047329fd3e2B7334`](https://optimistic.etherscan.io/address/0xFe57042De76c8D6B1DF0E9E2047329fd3e2B7334) (impl) - StETH ERC20RebasableBridgedPermit: [`0x76A50b8c7349cCDDb7578c6627e79b5d99D24138`](https://optimistic.etherscan.io/address/0x76A50b8c7349cCDDb7578c6627e79b5d99D24138) (proxy) - StETH ERC20RebasableBridgedPermit: [`0xe9b65dA5DcBe92f1b397991C464FF568Dc98D761`](https://optimistic.etherscan.io/address/0xe9b65dA5DcBe92f1b397991C464FF568Dc98D761) (impl) - TokenRateOracle: [`0x294ED1f214F4e0ecAE31C3Eae4F04EBB3b36C9d0`](https://optimistic.etherscan.io/address/0x294ED1f214F4e0ecAE31C3Eae4F04EBB3b36C9d0) (proxy) - TokenRateOracle: [`0x4bF0d419793d8722b8391efaD4c9cE78F460CEd3`](https://optimistic.etherscan.io/address/0x4bF0d419793d8722b8391efaD4c9cE78F460CEd3) (impl) - L2ERC20ExtendedTokensBridge: [`0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957`](https://optimistic.etherscan.io/address/0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957) (proxy) - L2ERC20ExtendedTokensBridge: [`0x2734602C0CEbbA68662552CacD5553370B283E2E`](https://optimistic.etherscan.io/address/0x2734602C0CEbbA68662552CacD5553370B283E2E) (impl) - Optimism Governance Bridge Executor: [`0xefa0db536d2c8089685630fafe88cf7805966fc3`](https://optimistic.etherscan.io/address/0xefa0db536d2c8089685630fafe88cf7805966fc3) ### ๐ŸŸฆ Base {#base} #### ๐Ÿงฑ Ethereum part {#ethereum-part-base} - L1ERC20TokenBridge: [`0x9de443AdC5A411E83F1878Ef24C3F52C61571e72`](https://etherscan.io/address/0x9de443AdC5A411E83F1878Ef24C3F52C61571e72) (proxy) - L1ERC20TokenBridge: [`0x313819736457910ac1dd21a712a37f3d7595645a`](https://etherscan.io/address/0x313819736457910ac1dd21a712a37f3d7595645a) (impl) #### ๐ŸŸฆ Base part {#base-part} - WstETH ERC20Bridged: [`0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452`](https://basescan.org/address/0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452) (proxy) - WstETH ERC20Bridged: [`0x69ce2505ce515c0203160450157366f927243309`](https://basescan.org/address/0x69ce2505ce515c0203160450157366f927243309) (impl) - L2ERC20TokenBridge: [`0xac9D11cD4D7eF6e54F14643a393F68Ca014287AB`](https://basescan.org/address/0xac9D11cD4D7eF6e54F14643a393F68Ca014287AB) (proxy) - L2ERC20TokenBridge: [`0x7063ef4f2887586e96096d3e94c9b6961c50a9a2`](https://basescan.org/address/0x7063ef4f2887586e96096d3e94c9b6961c50a9a2) (impl) - Base Governance Bridge Executor (`OptimismBridgeExecutor` contract is used): [`0x0E37599436974a25dDeEdF795C848d30Af46eaCF`](https://basescan.org/address/0x0E37599436974a25dDeEdF795C848d30Af46eaCF) ### ๐Ÿ“ Linea {#linea} #### ๐Ÿงฑ Ethereum part {#ethereum-part-linea} - L1 TokenBridge (Canonical Bridge): [`0x051f1d88f0af5763fb888ec4378b4d8b29ea3319`](https://etherscan.io/address/0x051f1d88f0af5763fb888ec4378b4d8b29ea3319) (proxy) - L1 TokenBridge (Canonical Bridge): [`0x2B6A2F8880220a66DfB9059FCB76F7dB54104a34`](https://etherscan.io/address/0x2B6A2F8880220a66DfB9059FCB76F7dB54104a34) (impl) - ProxyAdmin for L1 TokenBridge: [`0xF5058616517C068C7b8c7EbC69FF636Ade9066d6`](https://etherscan.io/address/0xF5058616517C068C7b8c7EbC69FF636Ade9066d6) #### ๐Ÿ“ Linea part {#linea-part} - wstETH CustomBridgedToken: [`0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F`](https://lineascan.build/address/0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F) (proxy) - wstETH CustomBridgedToken: [`0xc0583e2F5930EDE5Fab9D57bAC4169878730B010`](https://lineascan.build/address/0xc0583e2f5930ede5fab9d57bac4169878730b010) (impl) - ProxyAdmin for wstETH CustomBridgedToken: [`0xF951d7592e03eDB0Bab3D533935e678Ce64Eb927`](https://lineascan.build/address/0xF951d7592e03eDB0Bab3D533935e678Ce64Eb927) - L2 TokenBridge (Canonical Bridge): [`0x353012dc4a9a6cf55c941badc267f82004a8ceb9`](https://lineascan.build/address/0x353012dc4a9a6cf55c941badc267f82004a8ceb9) (proxy) - L2 TokenBridge (Canonical Bridge): [`0xd90ed3d4f9d11262d3d346a4369058d5b3777137`](https://lineascan.build/address/0xd90ed3d4f9d11262d3d346a4369058d5b3777137) (impl) - ProxyAdmin for L2 TokenBridge: [`0x1e1f6f22f97b4a7522d8b62e983953639239774e`](https://lineascan.build/address/0x1e1f6f22f97b4a7522d8b62e983953639239774e) - Linea Governance Bridge Executor: [`0x74Be82F00CC867614803ffd7f36A2a4aF0405670`](https://lineascan.build/address/0x74Be82F00CC867614803ffd7f36A2a4aF0405670) ### ๐ŸŸก Binance Smart Chain (BSC) {#binance-smart-chain-bsc} ##### ๐Ÿงฑ Ethereum part {#ethereum-part-bsc} ###### ๐Ÿ“ก a.DI governance forwarding {#adi-governance-forwarding-eth} - CrossChainController: [`0x93559892D3C7F66DE4570132d68b69BD3c369A7C`](https://etherscan.io/address/0x93559892D3C7F66DE4570132d68b69BD3c369A7C) (proxy) - CrossChainController: [`0x5f456f29238F8d63b3ae69bCEF9e9d4E953f2c63`](https://etherscan.io/address/0x5f456f29238F8d63b3ae69bCEF9e9d4E953f2c63) (impl) - ProxyAdmin [`0xADD673dC6A655AFD6f38fB88301028fA31A6fDeE`](https://etherscan.io/address/0xADD673dC6A655AFD6f38fB88301028fA31A6fDeE) for CrossChainController - CCIPAdapter: [`0x29D4fA5FCC282ba2788A281860770c166F597d5d`](https://etherscan.io/address/0x29D4fA5FCC282ba2788A281860770c166F597d5d) - HyperLaneAdapter: [`0x8d374DF3de08b971777Aa091fA68BCE109b3a7F3`](https://etherscan.io/address/0x8d374DF3de08b971777Aa091fA68BCE109b3a7F3) - LayerZeroAdapter: [`0x742650E0441Be8503682965d601AD0Ba1fB54411`](https://etherscan.io/address/0x742650E0441Be8503682965d601AD0Ba1fB54411) - WormholeAdapter: [`0xEDc0D2cb2289BBa1587424dd42bDD1ca7eAbDF17`](https://etherscan.io/address/0xEDc0D2cb2289BBa1587424dd42bDD1ca7eAbDF17) ###### ๐Ÿ”Œ wstETH on BSC endpoints {#wsteth-on-bsc-endpoints-eth} - NTT Manager: [`0xb948a93827d68a82F6513Ad178964Da487fe2BD9`](https://etherscan.io/address/0xb948a93827d68a82F6513Ad178964Da487fe2BD9) (proxy) - NTT Manager: [`0xc6c1f091450b54af3280cfed790047431bc99bb1`](https://etherscan.io/address/0xc6c1f091450b54af3280cfed790047431bc99bb1) (impl) - Wormhole Transceiver: [`0xA1ACC1e6edaB281Febd91E3515093F1DE81F25c0`](https://etherscan.io/address/0xA1ACC1e6edaB281Febd91E3515093F1DE81F25c0) - Axelar Transceiver: [`0x723AEAD29acee7E9281C32D11eA4ed0070c41B13`](https://etherscan.io/address/0x723AEAD29acee7E9281C32D11eA4ed0070c41B13) - Transceiver Structs (used by NTT Manager and Wormhole Transceiver): [`0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b`](https://etherscan.io/address/0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b) - Transceiver Structs (used by Axelar Transceiver): [`0xa12bc993d8144404a8c8c812816048275a066ced`](https://etherscan.io/address/0xa12bc993d8144404a8c8c812816048275a066ced) ##### ๐ŸŸก BSC part {#bsc-part} ###### ๐Ÿ“ก a.DI governance forwarding {#adi-governance-forwarding-bsc} - CrossChainController: [`0x40C4464fCa8caCd550C33B39d674fC257966022F`](https://bscscan.com/address/0x40C4464fCa8caCd550C33B39d674fC257966022F) (proxy) - CrossChainController: [`0xB7Ba81dd07885ae7BFD18452B36D3404d7EDD8Ee`](https://bscscan.com/address/0xB7Ba81dd07885ae7BFD18452B36D3404d7EDD8Ee) (impl) - ProxyAdmin [`0x29E6817db339795766244B96aEf5Dc534a98518d`](https://bscscan.com/address/0x29E6817db339795766244B96aEf5Dc534a98518d) for CrossChainController - CrossChainExecutor: [`0x8E5175D17f74d1D512de59b2f5d5A5d8177A123d`](https://bscscan.com/address/0x8E5175D17f74d1D512de59b2f5d5A5d8177A123d) - CCIPAdapter: [`0x15AD245133568c2498c7dA0cf2204A03b0e9b98A`](https://bscscan.com/address/0x15AD245133568c2498c7dA0cf2204A03b0e9b98A) - HyperLaneAdapter: [`0xCd867B440c726461e5fAbe8d3a050b2f8701C230`](https://bscscan.com/address/0xCd867B440c726461e5fAbe8d3a050b2f8701C230) - LayerZeroAdapter: [`0xc934433f4c433Cf80DE6fB65fd70C7a650D8a408`](https://bscscan.com/address/0xc934433f4c433Cf80DE6fB65fd70C7a650D8a408) - WormholeAdapter: [`0xBb1E43408BbF2C767Ff3Bd5bBC34E183CC1Ef119`](https://bscscan.com/address/0xBb1E43408BbF2C767Ff3Bd5bBC34E183CC1Ef119) ###### ๐Ÿ”Œ wstETH on BSC endpoints {#wsteth-on-bsc-endpoints-bsc} - WstEthL2Token: [`0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C`](https://bscscan.com/address/0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C) (proxy) - WstEthL2Token: [`0x451d447776778870bdfe76d031689703aba73ee5`](https://bscscan.com/address/0x451d447776778870bdfe76d031689703aba73ee5) (impl) - NTT Manager: [`0x6981F5621691CBfE3DdD524dE71076b79F0A0278`](https://bscscan.com/address/0x6981F5621691CBfE3DdD524dE71076b79F0A0278) (proxy) - NTT Manager: [`0xe82c2a5846cfb6d8683d6b636719e7aa61486838`](https://bscscan.com/address/0xe82c2a5846cfb6d8683d6b636719e7aa61486838) (impl) - Wormhole Transceiver: [`0xbe3F7e06872E0dF6CD7FF35B7aa4Bb1446DC9986`](https://bscscan.com/address/0xbe3F7e06872E0dF6CD7FF35B7aa4Bb1446DC9986) - Axelar Transceiver: [`0x723AEAD29acee7E9281C32D11eA4ed0070c41B13`](https://bscscan.com/address/0x723AEAD29acee7E9281C32D11eA4ed0070c41B13) - Transceiver Structs (used by NTT Manager and Wormhole Transceiver): [`0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b`](https://bscscan.com/address/0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b) - Transceiver Structs (used by Axelar Transceiver): [`0x27a3daf3b243104e9b0afae6b56026a416b852c9`](https://bscscan.com/address/0x27a3daf3b243104e9b0afae6b56026a416b852c9) ### ๐Ÿ”— Unichain {#unichain} ##### ๐Ÿงฑ Ethereum part {#ethereum-part-unichain} - OpStackTokenRatePusher: [`0x3F9600439Ad97fC6f55C2AC7C118f8Fd0595eB74`](https://etherscan.io/address/0x3F9600439Ad97fC6f55C2AC7C118f8Fd0595eB74) - L1LidoTokensBridge: [`0x755610f5Be536Ad7afBAa7c10F3E938Ea3aa1877`](https://etherscan.io/address/0x755610f5Be536Ad7afBAa7c10F3E938Ea3aa1877) (proxy) - L1LidoTokensBridge: [`0x6078232C54d956c901620fa4590e0F7E37c2B82f`](https://etherscan.io/address/0x6078232C54d956c901620fa4590e0F7E37c2B82f) (impl) ##### ๐Ÿ”— Unichain part {#unichain-part} - WstETH ERC20BridgedPermit: [`0xc02fE7317D4eb8753a02c35fe019786854A92001`](https://uniscan.xyz/address/0xc02fE7317D4eb8753a02c35fe019786854A92001) (proxy) - WstETH ERC20BridgedPermit: [`0xB5CF096A406C1D5297D2493073168F44EB4a1A1d`](https://uniscan.xyz/address/0xB5CF096A406C1D5297D2493073168F44EB4a1A1d) (impl) - StETH ERC20RebasableBridgedPermit: [`0x81f2508AAC59757EF7425DDc9717AB5c2AA0A84F`](https://uniscan.xyz/address/0x81f2508AAC59757EF7425DDc9717AB5c2AA0A84F) (proxy) - StETH ERC20RebasableBridgedPermit: [`0x5A007D6E37633FB297b82c074b94Bb29546BEbc3`](https://uniscan.xyz/address/0x5A007D6E37633FB297b82c074b94Bb29546BEbc3) (impl) - TokenRateOracle: [`0xD835fAC9080396CCE95bDf9EcC7cc27Bab12c9f8`](https://uniscan.xyz/address/0xD835fAC9080396CCE95bDf9EcC7cc27Bab12c9f8) (proxy) - TokenRateOracle: [`0x537A7F9D551da3C2800cB11ca17f2946D21029AF`](https://uniscan.xyz/address/0x537A7F9D551da3C2800cB11ca17f2946D21029AF) (impl) - L2ERC20ExtendedTokensBridge: [`0x1A513e9B6434a12C7bB5B9AF3B21963308DEE372`](https://uniscan.xyz/address/0x1A513e9B6434a12C7bB5B9AF3B21963308DEE372) (proxy) - L2ERC20ExtendedTokensBridge: [`0x332CA368dd09AD309c51dC6350730e0Bca85CffE`](https://uniscan.xyz/address/0x332CA368dd09AD309c51dC6350730e0Bca85CffE) (impl) - Unichain Governance Bridge Executor: [`0x3b00f262e39372DF2756f809DD5DC36aeEdFC4A0`](https://uniscan.xyz/address/0x3b00f262e39372DF2756f809DD5DC36aeEdFC4A0) ### ๐ŸŠ Lido Multichain Liquidity pools {#lido-multichain-liquidity-pools} Balancer - [wstETH/WETH](https://balancer.fi/pools/arbitrum/v2/0xfb5e6d0c1dfed2ba000fbc040ab8df3615ac329c000000000000000000000159) on Arbitrum: [`0xFB5e6d0c1DfeD2BA000fBC040Ab8DF3615AC329c`](https://arbiscan.io/address/0xfb5e6d0c1dfed2ba000fbc040ab8df3615ac329c) - [wstETH/USDC](https://balancer.fi/pools/arbitrum/v2/0x178e029173417b1f9c8bc16dcec6f697bc323746000200000000000000000158) on Arbitrum: [`0x178E029173417b1F9C8bC16DCeC6f697bC323746`](https://arbiscan.io/address/0x178e029173417b1f9c8bc16dcec6f697bc323746) Kyber Network - [wstETH/ETH](https://kyberswap.com/elastic/add/0x5979d7b546e38e414f7e9822514be443a4800529/ETH/40) on Arbitrum: [`0x2149a5f5d7ca96eb98a2ee6e5b0ba1a5593a1a0a`](https://arbiscan.io/address/0x2149a5f5d7ca96eb98a2ee6e5b0ba1a5593a1a0a) - [wstETH/USDC](https://kyberswap.com/elastic/add/0x5979d7b546e38e414f7e9822514be443a4800529/0xff970a61a04b1ca14834a43f5de4533ebddb5cc8/40) on Arbitrum: [`0x7acbea3b8ab7cdf4a595c6ed81e7d3e26038d494`](https://arbiscan.io/address/0x7acbea3b8ab7cdf4a595c6ed81e7d3e26038d494) - [wstETH/ETH](https://kyberswap.com/elastic/add/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb/ETH/10) on Optimism: [`0xda74db17023750d02b83be2559a4eaa013b65c54`](https://optimistic.etherscan.io/address/0xda74db17023750d02b83be2559a4eaa013b65c54) - [wstETH/USDC](https://kyberswap.com/elastic/add/0x5979D7b546E38E414F7E9822514be443A4800529/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/40) on Optimism: [`0x5fc53f707c7aacd460a1cd564c06e0f07610fcb7`](https://optimistic.etherscan.io/address/0x5fc53f707c7aacd460a1cd564c06e0f07610fcb7) ## ๐Ÿ”— CCIP Direct Staking {#ccip-direct-staking} Chainlink's [CCIP Direct Staking](https://docs.chain.link/quickstarts/ccip-direct-staking) related contracts. Note: Some addresses in the CCIP Direct Staking lists repeat across different networks; the linked explorer domain indicates the chain. ### ๐Ÿงฑ Ethereum (common) {#ethereum-common-ccip-ds} - LidoCustomReceiver: [`0x6F357d53d6bE3238180316BA5F8f11467e164588`](https://etherscan.io/address/0x6F357d53d6bE3238180316BA5F8f11467e164588) (proxy) - LidoCustomReceiver: [`0x301cBCDA894c932E9EDa3Cf8878f78304e69E367`](https://etherscan.io/address/0x301cBCDA894c932E9EDa3Cf8878f78304e69E367) (impl) - ProxyAdmin for LidoCustomReceiver: [`0x88a45d2760b63c1500E3D2E3552b28e5Cdaa37BD`](https://etherscan.io/address/0x88a45d2760b63c1500E3D2E3552b28e5Cdaa37BD) ### ๐ŸŒ€ Direct Staking on Arbitrum {#ccip-direct-staking-arbitrum} #### ๐Ÿงฑ Ethereum part {#ccip-direct-staking-arbitrum-ethereum} - ArbitrumLegacyAdapterL1toL2: [`0xBf96561e4519182CFA4cebBf95494D9CA5a316f9`](https://etherscan.io/address/0xBf96561e4519182CFA4cebBf95494D9CA5a316f9) #### ๐ŸŒ€ Arbitrum part {#ccip-direct-staking-arbitrum-l2} - CustomSenderReferral: [`0x72229141D4B016682d3618ECe47c046f30Da4AD1`](https://arbiscan.io/address/0x72229141D4B016682d3618ECe47c046f30Da4AD1) (proxy) - CustomSenderReferral: [`0x220F64A4793Bc8aca7330ceCc4ae4e2F3B5Bc664`](https://arbiscan.io/address/0x220F64A4793Bc8aca7330ceCc4ae4e2F3B5Bc664) (impl) - ProxyAdmin for CustomSenderReferral: [`0x5B42aEbFe95247f1d22e282831e2A513bF050217`](https://arbiscan.io/address/0x5B42aEbFe95247f1d22e282831e2A513bF050217) - PausableImmutableOraclePool: [`0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8`](https://arbiscan.io/address/0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8) - SyncTrigger: [`0x871a5cddE9813627Ff37A2895A0c9B117A664622`](https://arbiscan.io/address/0x871a5cddE9813627Ff37A2895A0c9B117A664622) - CREReceiver: [`0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A`](https://arbiscan.io/address/0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A) ### ๐ŸŒž Direct Staking on Optimism {#ccip-direct-staking-optimism} #### ๐Ÿงฑ Ethereum part {#ccip-direct-staking-optimism-ethereum} - OptimismLegacyAdapterL1toL2: [`0x328de900860816d29D1367F6903a24D8ed40C997`](https://etherscan.io/address/0x328de900860816d29D1367F6903a24D8ed40C997) #### ๐ŸŒž Optimism part {#ccip-direct-staking-optimism-l2} - CustomSenderReferral: [`0x328de900860816d29D1367F6903a24D8ed40C997`](https://optimistic.etherscan.io/address/0x328de900860816d29D1367F6903a24D8ed40C997) (proxy) - CustomSenderReferral: [`0x65498495DdC07c52E12EEe3c44D3a1166eed8703`](https://optimistic.etherscan.io/address/0x65498495DdC07c52E12EEe3c44D3a1166eed8703) (impl) - ProxyAdmin for CustomSenderReferral: [`0x4c8c4A15c1e810e481c412A9B06Be5f79dC02192`](https://optimistic.etherscan.io/address/0x4c8c4A15c1e810e481c412A9B06Be5f79dC02192) - PausableImmutableOraclePool: [`0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8`](https://optimistic.etherscan.io/address/0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8) - SyncTrigger: [`0x871a5cddE9813627Ff37A2895A0c9B117A664622`](https://optimistic.etherscan.io/address/0x871a5cddE9813627Ff37A2895A0c9B117A664622) - CREReceiver: [`0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A`](https://optimistic.etherscan.io/address/0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A) ### ๐ŸŸฆ Direct Staking on Base {#ccip-direct-staking-base} #### ๐Ÿงฑ Ethereum part {#ccip-direct-staking-base-ethereum} - BaseLegacyAdapterL1toL2: [`0x9c27c304cFdf0D9177002ff186A4aE0A5489Aace`](https://etherscan.io/address/0x9c27c304cFdf0D9177002ff186A4aE0A5489Aace) #### ๐ŸŸฆ Base part {#ccip-direct-staking-base-l2} - CustomSenderReferral: [`0x328de900860816d29D1367F6903a24D8ed40C997`](https://basescan.org/address/0x328de900860816d29D1367F6903a24D8ed40C997) (proxy) - CustomSenderReferral: [`0x65498495DdC07c52E12EEe3c44D3a1166eed8703`](https://basescan.org/address/0x65498495DdC07c52E12EEe3c44D3a1166eed8703) (impl) - ProxyAdmin for CustomSenderReferral: [`0x4c8c4A15c1e810e481c412A9B06Be5f79dC02192`](https://basescan.org/address/0x4c8c4A15c1e810e481c412A9B06Be5f79dC02192) - PausableImmutableOraclePool: [`0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8`](https://basescan.org/address/0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8) - SyncTrigger: [`0x871a5cddE9813627Ff37A2895A0c9B117A664622`](https://basescan.org/address/0x871a5cddE9813627Ff37A2895A0c9B117A664622) - CREReceiver: [`0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A`](https://basescan.org/address/0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A) ### ๐Ÿ“ Direct Staking on Linea {#ccip-direct-staking-linea} #### ๐Ÿงฑ Ethereum part {#ccip-direct-staking-linea-ethereum} - LineaAdapterL1toL2: [`0x122beD1eB48DC4679DDF2C8fc159e9c498344397`](https://etherscan.io/address/0x122beD1eB48DC4679DDF2C8fc159e9c498344397) #### ๐Ÿ“ Linea part {#ccip-direct-staking-linea-l2} - CustomSenderReferral: [`0x328de900860816d29D1367F6903a24D8ed40C997`](https://lineascan.build/address/0x328de900860816d29D1367F6903a24D8ed40C997) (proxy) - CustomSenderReferral: [`0xBf96561e4519182CFA4cebBf95494D9CA5a316f9`](https://lineascan.build/address/0xBf96561e4519182CFA4cebBf95494D9CA5a316f9) (impl) - ProxyAdmin for CustomSenderReferral: [`0x4c8c4A15c1e810e481c412A9B06Be5f79dC02192`](https://lineascan.build/address/0x4c8c4A15c1e810e481c412A9B06Be5f79dC02192) - PausableImmutableOraclePool: [`0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8`](https://lineascan.build/address/0xac143bF41BBA4a8014b4Ef5a5F46b39a36AE40A8) - SyncTrigger: [`0x871a5cddE9813627Ff37A2895A0c9B117A664622`](https://lineascan.build/address/0x871a5cddE9813627Ff37A2895A0c9B117A664622) - CREReceiver: [`0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A`](https://lineascan.build/address/0x09BdB4E8BA68d245DCb1c6fbEb1e4f13b57cc69A) ## ๐Ÿ”’ LRT Vaults on Mellow Protocol {#lrt-vaults-on-mellow-protocol} There's a joint bug bounty for the vaults deployed at addresses listed in [the deployment verification audit record](https://github.com/mellow-finance/mellow-lrt/blob/85370ae372f95d057dc9806ec98fde24e5ed4d29/audits/202406_Statemind/Mellow%20LRT%20report%20with%20deployment.pdf). Any findings regarding the code deployed on those addresses can be reported to [the Lido Immunefi Bug bounty](https://immunefi.com/bug-bounty/lido/) with thresholds of up to $500k on critical finding. ## ๐Ÿ“œ Legacy Contracts {#legacy-contracts} > **These contracts were previously used and are currently active, but they are no longer being supported by contributors from Lido DAO and don't fall under Lido bug bounty scope and terms anymore** - Finance Ops: [`0x48F300bD3C52c7dA6aAbDE4B683dEB27d38B9ABb`](https://etherscan.io/address/0x48F300bD3C52c7dA6aAbDE4B683dEB27d38B9ABb) - Cover Ops: [`0xD089cc83f5B803993E266ACEB929e52A993Ca2C8`](https://etherscan.io/address/0xD089cc83f5B803993E266ACEB929e52A993Ca2C8) - 1inch Liquidity Farming: [`0xdB46C277dA1599390eAb394327602889E9546296`](https://etherscan.io/address/0xdB46C277dA1599390eAb394327602889E9546296) - AnchorVault: [`0xA2F987A546D4CD1c607Ee8141276876C26b72Bdf`](https://etherscan.io/address/0xA2F987A546D4CD1c607Ee8141276876C26b72Bdf) - AnchorVault Implementation: [`0x9530708033E7262bD7c005d0e0D47D8A9184277d`](https://etherscan.io/address/0x9530708033E7262bD7c005d0e0D47D8A9184277d) - bETH Token: [`0x707f9118e33a9b8998bea41dd0d46f38bb963fc8`](https://etherscan.io/address/0x707f9118e33a9b8998bea41dd0d46f38bb963fc8) - ARCx: - Manager Contract: [`0x6140182B2536AE7B6Cfcfb2d2bAB0f6Fe0D7b58E`](https://etherscan.io/address/0x6140182B2536AE7B6Cfcfb2d2bAB0f6Fe0D7b58E) - Reward Contract: [`0x8F1155447Ee97b5Ae147a01a5c420B0FDDF0370D`](https://etherscan.io/address/0x8F1155447Ee97b5Ae147a01a5c420B0FDDF0370D) - SushiSwap LP rewards: - Manager Contract: [`0xE5576eB1dD4aA524D67Cf9a32C8742540252b6F4`](https://etherscan.io/address/0xe5576eb1dd4aa524d67cf9a32c8742540252b6f4) - Reward Contract: [`0x75ff3dd673Ef9fC459A52E1054db5dF2A1101212`](https://etherscan.io/address/0x75ff3dd673ef9fc459a52e1054db5df2a1101212) - Balancer LP rewards v1: - Manager Contract: [`0x1dD909cDdF3dbe61aC08112dC0Fdf2Ab949f79D8`](https://etherscan.io/address/0x1dD909cDdF3dbe61aC08112dC0Fdf2Ab949f79D8) - 1inch LP rewards manager: - Manager Contract: [`0xf5436129cf9d8fa2a1cb6e591347155276550635`](https://etherscan.io/address/0xf5436129cf9d8fa2a1cb6e591347155276550635) - Balancer LP rewards v2: - Manager Contract: [`0x1220ccCDc9BBA5CF626a84586C74D6f940932342`](https://etherscan.io/address/0x1220ccCDc9BBA5CF626a84586C74D6f940932342) - Reward Contract: [`0x55c8De1Ac17C1A937293416C9BCe5789CbBf61d1`](https://etherscan.io/address/0x55c8De1Ac17C1A937293416C9BCe5789CbBf61d1) - Treasury Diversification: [2021 May round](https://research.lido.fi/t/proposal-ldo-treasury-diversification/458) [`0x489F04EEff0ba8441D42736549A1f1d6ccA74775`](https://etherscan.io/address/0x489F04EEff0ba8441D42736549A1f1d6ccA74775) & [2021 October run](https://research.lido.fi/t/lido-treasury-diversification-part-3/1059/1) [`0x689E03565e36B034EcCf12d182c3DC38b2Bb7D33`](https://etherscan.io/address/0x689E03565e36B034EcCf12d182c3DC38b2Bb7D33). - Treasury Diversification Part 2: [2022 Aug round](https://research.lido.fi/t/treasury-diversification-2-part-2/2657) [`0xA9b2F5ce3aAE7374a62313473a74C98baa7fa70E`](https://etherscan.io/address/0xA9b2F5ce3aAE7374a62313473a74C98baa7fa70E). - [Lido Contributors Group Multisigs](/multisigs/lido-contributors-group.md) - Pool Maintenance Labs Ltd. (PML): [`0x17F6b2C738a63a8D3A113a228cfd0b373244633D`](https://app.safe.global/settings/setup?safe=eth:0x17F6b2C738a63a8D3A113a228cfd0b373244633D) - Argo Technology Consulting Ltd. (ATC): [`0x9B1cebF7616f2BC73b47D226f90b01a7c9F86956`](https://app.safe.global/settings/setup?safe=eth:0x9B1cebF7616f2BC73b47D226f90b01a7c9F86956) - Resourcing and Compensation Committee (RCC): [`0xDE06d17Db9295Fa8c4082D4f73Ff81592A3aC437`](https://app.safe.global/settings/setup?safe=eth:0xDE06d17Db9295Fa8c4082D4f73Ff81592A3aC437) - Easy Track factory for token transfers: RCC stablecoins (committee ms [`0xDE06d17Db9295Fa8c4082D4f73Ff81592A3aC437`](https://app.safe.global/settings/setup?safe=eth:0xDE06d17Db9295Fa8c4082D4f73Ff81592A3aC437)) - AllowedRecipientsRegistry: [`0xDc1A0C7849150f466F07d48b38eAA6cE99079f80`](https://etherscan.io/address/0xDc1A0C7849150f466F07d48b38eAA6cE99079f80) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0x75bDecbb6453a901EBBB945215416561547dfDD4`](https://etherscan.io/address/0x75bDecbb6453a901EBBB945215416561547dfDD4) - Easy Track factory for token transfers: RCC stETH (committee ms [`0xDE06d17Db9295Fa8c4082D4f73Ff81592A3aC437`](https://app.safe.global/settings/setup?safe=eth:0xDE06d17Db9295Fa8c4082D4f73Ff81592A3aC437)) - AllowedRecipientsRegistry: [`0xAAC4FcE2c5d55D1152512fe5FAA94DB267EE4863`](https://etherscan.io/address/0xAAC4FcE2c5d55D1152512fe5FAA94DB267EE4863) - TopUpAllowedRecipients: [`0xcD42Eb8a5db5a80Dc8f643745528DD77cf4C7D35`](https://etherscan.io/address/0xcD42Eb8a5db5a80Dc8f643745528DD77cf4C7D35) - Easy Track factory for token transfers: PML stablecoins (committee ms [`0x17F6b2C738a63a8D3A113a228cfd0b373244633D`](https://app.safe.global/settings/setup?safe=eth:0x17F6b2C738a63a8D3A113a228cfd0b373244633D)) - AllowedRecipientsRegistry: [`0xDFfCD3BF14796a62a804c1B16F877Cf7120379dB`](https://etherscan.io/address/0xDFfCD3BF14796a62a804c1B16F877Cf7120379dB) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0x92a27C4e5e35cFEa112ACaB53851Ec70e2D99a8D`](https://etherscan.io/address/0x92a27C4e5e35cFEa112ACaB53851Ec70e2D99a8D) - Easy Track factory for token transfers: PML stETH (committee ms [`0x17F6b2C738a63a8D3A113a228cfd0b373244633D`](https://app.safe.global/settings/setup?safe=eth:0x17F6b2C738a63a8D3A113a228cfd0b373244633D)) - AllowedRecipientsRegistry: [`0x7b9B8d00f807663d46Fb07F87d61B79884BC335B`](https://etherscan.io/address/0x7b9B8d00f807663d46Fb07F87d61B79884BC335B) - TopUpAllowedRecipients: [`0xc5527396DDC353BD05bBA578aDAa1f5b6c721136`](https://etherscan.io/address/0xc5527396DDC353BD05bBA578aDAa1f5b6c721136) - Easy Track factory for token transfers: ATC stablecoins (committee ms [`0x9B1cebF7616f2BC73b47D226f90b01a7c9F86956`](https://app.safe.global/settings/setup?safe=eth:0x9B1cebF7616f2BC73b47D226f90b01a7c9F86956)) - AllowedRecipientsRegistry: [`0xe07305F43B11F230EaA951002F6a55a16419B707`](https://etherscan.io/address/0xe07305F43B11F230EaA951002F6a55a16419B707) - AllowedTokensRegistry: [`0x4AC40c34f8992bb1e5E856A448792158022551ca`](https://etherscan.io/address/0x4AC40c34f8992bb1e5E856A448792158022551ca) - TopUpAllowedRecipients: [`0x1843Bc35d1fD15AbE1913b9f72852a79457C42Ab`](https://etherscan.io/address/0x1843Bc35d1fD15AbE1913b9f72852a79457C42Ab) - Easy Track factory for token transfers: ATC stETH (committee ms [`0x9B1cebF7616f2BC73b47D226f90b01a7c9F86956`](https://app.safe.global/settings/setup?safe=eth:0x9B1cebF7616f2BC73b47D226f90b01a7c9F86956)) - AllowedRecipientsRegistry: [`0xd3950eB3d7A9B0aBf8515922c0d35D13e85a2c91`](https://etherscan.io/address/0xd3950eB3d7A9B0aBf8515922c0d35D13e85a2c91) - TopUpAllowedRecipients: [`0x87b02dF27cd6ec128532Add7C8BC19f62E6f1fB9`](https://etherscan.io/address/0x87b02dF27cd6ec128532Add7C8BC19f62E6f1fB9) - Dual Governance upgrade contracts - Dual Governance Upgrade State Verifier: [`0x487b764a2085ffd595D9141BAec0A766B7904786`](https://etherscan.io/address/0x487b764a2085ffd595D9141BAec0A766B7904786) - Dual Governance Upgrade Omnibus Provider: [`0x67988077f29FbA661911d9567E05cc52C51ca1B0`](https://etherscan.io/address/0x67988077f29FbA661911d9567E05cc52C51ca1B0) - Dual Governance Config Provider for disconnected DualGovernance contract: [`0xc934E90E76449F09f2369BB85DCEa056567A327a`](https://etherscan.io/address/0xc934E90E76449F09f2369BB85DCEa056567A327a) - Lido V3 upgrade contracts: - V3 Temporary Admin: [`0xf738A2C7d69694B618dbB547C1c5A152D7958f06`](https://etherscan.io/address/0xf738A2C7d69694B618dbB547C1c5A152D7958f06) - V3 Template: [`0x34E01ecFebd403370b0879C628f8A5319dDb8507`](https://etherscan.io/address/0x34E01ecFebd403370b0879C628f8A5319dDb8507) - V3 Vote Script: [`0xE1F4c16908fCE6935b5Ad38C6e3d58830fe86442`](https://etherscan.io/address/0xE1F4c16908fCE6935b5Ad38C6e3d58830fe86442) - zkSync Era: - L1Executor: [`0xFf7F4d05e3247374e86A3f7231A2Ed1CA63647F2`](https://etherscan.io/address/0xFf7F4d05e3247374e86A3f7231A2Ed1CA63647F2) (proxy) - L1Executor: [`0x06185d60eD72a91D1367Eb0733B9d20AE7336D3B`](https://etherscan.io/address/0x06185d60eD72a91D1367Eb0733B9d20AE7336D3B) (impl) - L1ERC20Bridge: [`0x41527B2d03844dB6b0945f25702cB958b6d55989`](https://etherscan.io/address/0x41527B2d03844dB6b0945f25702cB958b6d55989) (proxy) - L1ERC20Bridge: [`0x43a66b32c9adca1a59b273e69b61da5197c21ccd`](https://etherscan.io/address/0x43a66b32c9adca1a59b273e69b61da5197c21ccd) (impl) - zkSync Governance Bridge Executor: [`0x139EE25DCad405d2a038E7A67f9ffdbf0f573f3c`](https://explorer.zksync.io/address/0x139EE25DCad405d2a038E7A67f9ffdbf0f573f3c) (proxy) - zkSync Governance Bridge Executor: [`0x13f46b59067f064c634fb17e207ed203916dccc8`](https://explorer.zksync.io/address/0x13f46b59067f064c634fb17e207ed203916dccc8) (impl) - L2ERC20Bridge: [`0xE1D6A50E7101c8f8db77352897Ee3f1AC53f782B`](https://explorer.zksync.io/address/0xE1D6A50E7101c8f8db77352897Ee3f1AC53f782B) (proxy) - L2ERC20Bridge: [`0x64Ee90B086c99fD3439354f382Fef25229A01F02`](https://explorer.zksync.io/address/0x64Ee90B086c99fD3439354f382Fef25229A01F02) (impl) - ERC20BridgedUpgradeable: [`0x703b52F2b28fEbcB60E1372858AF5b18849FE867`](https://explorer.zksync.io/address/0x703b52F2b28fEbcB60E1372858AF5b18849FE867) (proxy) - ERC20BridgedUpgradeable: [`0xc7a0daa1b8fea68532b6425d0e156088b0d2ab2c`](https://explorer.zksync.io/address/0xc7a0daa1b8fea68532b6425d0e156088b0d2ab2c) (impl) - ProxyAdmin: [`0xbd80e505ecc49bae2cc86094a78fa0e2db28b52a`](https://explorer.zksync.io/address/0xbd80e505ecc49bae2cc86094a78fa0e2db28b52a) - Mode: - L1ERC20TokenBridge: [`0xD0DeA0a3bd8E4D55170943129c025d3fe0493F2A`](https://etherscan.io/address/0xD0DeA0a3bd8E4D55170943129c025d3fe0493F2A) (proxy) - L1ERC20TokenBridge: [`0xE6A4ED59Ec73eD78aE3A10294c99F0EE18A6bF76`](https://etherscan.io/address/0xE6A4ED59Ec73eD78aE3A10294c99F0EE18A6bF76) (impl) - WstETH ERC20Bridged: [`0x98f96A4B34D03a2E6f225B28b8f8Cb1279562d81`](https://explorer.mode.network/address/0x98f96A4B34D03a2E6f225B28b8f8Cb1279562d81) (proxy) - WstETH ERC20Bridged: [`0xF27b1B121e55A13047d66dC4AAA8c17BA72c762A`](https://explorer.mode.network/address/0xF27b1B121e55A13047d66dC4AAA8c17BA72c762A) (impl) - L2ERC20TokenBridge: [`0xb8161F28a5a38cE58f155D9A96bDAc0104985FAc`](https://explorer.mode.network/address/0xb8161F28a5a38cE58f155D9A96bDAc0104985FAc) (proxy) - L2ERC20TokenBridge: [`0x488cDB57E9a1006ab77730fC8b19e1BB76e1cB97`](https://explorer.mode.network/address/0x488cDB57E9a1006ab77730fC8b19e1BB76e1cB97) (impl) - Mode Governance Bridge Executor: [`0x2aCeC6D8ABA90685927b61968D84CfFf6192B32C`](https://explorer.mode.network/address/0x2aCeC6D8ABA90685927b61968D84CfFf6192B32C) - Scroll: - L1LidoGateway: [`0x6625c6332c9f91f2d27c304e729b86db87a3f504`](https://etherscan.io/address/0x6625c6332c9f91f2d27c304e729b86db87a3f504) (proxy) - L1LidoGateway: [`0xF4f2066EE72D62e3caF9678459149BA7FCf2262F`](https://etherscan.io/address/0xF4f2066EE72D62e3caF9678459149BA7FCf2262F) (impl) - ProxyAdmin: [`0xCC2C53556Bc75217cf698721b29071d6f12628A9`](https://etherscan.io/address/0xCC2C53556Bc75217cf698721b29071d6f12628A9) - Scroll Governance Bridge Executor: [`0x0c67D8D067E349669dfEAB132A7c03A90594eE09`](https://scrollscan.com/address/0x0c67D8D067E349669dfEAB132A7c03A90594eE09) - L2LidoGateway: [`0x8aE8f22226B9d789A36AC81474e633f8bE2856c9`](https://scrollscan.com/address/0x8aE8f22226B9d789A36AC81474e633f8bE2856c9) (proxy) - L2LidoGateway: [`0x2B9beB2890DBeFC7cA25Af3164100d139B623C24`](https://scrollscan.com/address/0x2B9beB2890DBeFC7cA25Af3164100d139B623C24) (impl) - L2WstETHToken: [`0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32`](https://scrollscan.com/address/0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32) (proxy) - L2WstETHToken: [`0x38224D52ecC979aEdfEb31b1EEa0cfCEbd55247e`](https://scrollscan.com/address/0x38224D52ecC979aEdfEb31b1EEa0cfCEbd55247e) (impl) - ProxyAdmin: [`0x8e34D07Eb348716a1f0a48A507A9de8a3A6DcE45`](https://scrollscan.com/address/0x8e34D07Eb348716a1f0a48A507A9de8a3A6DcE45) - Mantle: - L1ERC20TokenBridge: [`0x2D001d79E5aF5F65a939781FE228B267a8Ed468B`](https://etherscan.io/address/0x2D001d79E5aF5F65a939781FE228B267a8Ed468B) (proxy) - L1ERC20TokenBridge: [`0x6fBBe1Af52D22557D7F161Dc5952E306F4742e23`](https://etherscan.io/address/0x6fBBe1Af52D22557D7F161Dc5952E306F4742e23) (impl) - WstETH ERC20BridgedPermit: [`0x458ed78EB972a369799fb278c0243b25e5242A83`](https://explorer.mantle.xyz/address/0x458ed78EB972a369799fb278c0243b25e5242A83) (proxy) - WstETH ERC20BridgedPermit: [`0x1FaBaAec88198291A4efCc85Cabb33a3785165ba`](https://explorer.mantle.xyz/address/0x1FaBaAec88198291A4efCc85Cabb33a3785165ba) (impl) - L2ERC20TokenBridge: [`0x9c46560D6209743968cC24150893631A39AfDe4d`](https://explorer.mantle.xyz/address/0x9c46560D6209743968cC24150893631A39AfDe4d) (proxy) - L2ERC20TokenBridge: [`0xf10A7ffC613a9b23Abc36167925A375bf5986181`](https://explorer.mantle.xyz/address/0xf10A7ffC613a9b23Abc36167925A375bf5986181) (impl) - Mantle Governance Bridge Executor: [`0x3a7b055bf88cdc59d20d0245809c6e6b3c5819dd`](https://explorer.mantle.xyz/address/0x3a7b055bf88cdc59d20d0245809c6e6b3c5819dd) - Swellchain: - L1ERC20TokenBridge: [`0xecf3376512EDAcA4FBB63d2c67d12a0397d24121`](https://etherscan.io/address/0xecf3376512EDAcA4FBB63d2c67d12a0397d24121) (proxy) - L1ERC20TokenBridge: [`0x7e97935FbDF2a27EA35c4fdDdaCf5ACd685e65A2`](https://etherscan.io/address/0x7e97935FbDF2a27EA35c4fdDdaCf5ACd685e65A2) (impl) - WstETH ERC20Bridged: `0x7c98E0779EB5924b3ba8cE3B17648539ed5b0Ecc` (proxy) - WstETH ERC20Bridged: `0xa1A3257813eD45d91e9c45E03C66FcDD54B4e7c1` (impl) - L2ERC20TokenBridge: `0x8311496799B8C2C7f13bC32c123ac4Eea068e6F0` (proxy) - L2ERC20TokenBridge: `0x66ca84bC3C2dB33b6bd7B8994C033444C72b8ADE` (impl) - Governance BridgeExecutor: `0xFF22ea467301010F1364fc154c13e0c86Fcfb077` - Zircuit: - L1ERC20TokenBridge: [`0x912C7271a6A3622dfb8B218eb46a6122aB046C79`](https://etherscan.io/address/0x912C7271a6A3622dfb8B218eb46a6122aB046C79) (proxy) - L1ERC20TokenBridge: [`0x6bc726C993103197C41d787dd72eCd4D2e1614E8`](https://etherscan.io/address/0x6bc726C993103197C41d787dd72eCd4D2e1614E8) (impl) - WstETH ERC20Bridged: [`0xf0e673Bc224A8Ca3ff67a61605814666b1234833`](https://explorer.zircuit.com/address/0xf0e673Bc224A8Ca3ff67a61605814666b1234833) (proxy) - WstETH ERC20Bridged: [`0x929569e10d9166f31c8284fE3FE5db1C1E56D6b4`](https://explorer.zircuit.com/address/0x929569e10d9166f31c8284fE3FE5db1C1E56D6b4) (impl) - L2ERC20TokenBridge: [`0xF4DC271cA48446a5d2b97Ff41D39918DF8A4Eb0e`](https://explorer.zircuit.com/address/0xF4DC271cA48446a5d2b97Ff41D39918DF8A4Eb0e) (proxy) - L2ERC20TokenBridge: [`0x224F00AEDD7A9F10e571898662ad19CD5abd9F2c`](https://explorer.zircuit.com/address/0x224F00AEDD7A9F10e571898662ad19CD5abd9F2c) (impl) - Zircuit Governance Bridge Executor: [`0x6Bf2cac3ed2481da30aD36Cd3D64325c31065Cc5`](https://explorer.zircuit.com/address/0x6Bf2cac3ed2481da30aD36Cd3D64325c31065Cc5) - Soneium: - OpStackTokenRatePusher: [`0x927C99fC46226bd5131420B16aF0b0371165C3FC`](https://etherscan.io/address/0x927C99fC46226bd5131420B16aF0b0371165C3FC) - L1LidoTokensBridge: [`0x2F543A7C9cc80Cc2427c892B96263098d23ee55a`](https://etherscan.io/address/0x2F543A7C9cc80Cc2427c892B96263098d23ee55a) (proxy) - L1LidoTokensBridge: [`0xf034dE8BD85A434d9Dc68F03382B589f86791425`](https://etherscan.io/address/0xf034dE8BD85A434d9Dc68F03382B589f86791425) (impl) - WstETH ERC20BridgedPermit: [`0xaA9BD8c957D803466FA92504BDd728cC140f8941`](https://soneium.blockscout.com/address/0xaA9BD8c957D803466FA92504BDd728cC140f8941) (proxy) - WstETH ERC20BridgedPermit: [`0x7591f6BD2301f7EE9267738039054047b5B395B0`](https://soneium.blockscout.com/address/0x7591f6BD2301f7EE9267738039054047b5B395B0) (impl) - StETH ERC20RebasableBridgedPermit: [`0x0Ce031AEd457C870D74914eCAA7971dd3176cDAF`](https://soneium.blockscout.com/address/0x0Ce031AEd457C870D74914eCAA7971dd3176cDAF) (proxy) - StETH ERC20RebasableBridgedPermit: [`0x3BC5d0551F48902bDcC036d59F5D23987F581c28`](https://soneium.blockscout.com/address/0x3BC5d0551F48902bDcC036d59F5D23987F581c28) (impl) - TokenRateOracle: [`0xDff6f372e8c16b2b9e95c55bDfe74C0bA3F90265`](https://soneium.blockscout.com/address/0xDff6f372e8c16b2b9e95c55bDfe74C0bA3F90265) (proxy) - TokenRateOracle: [`0xA2f12f7C109c0b9aa5FFAe71612a68B6b8B2eFC4`](https://soneium.blockscout.com/address/0xA2f12f7C109c0b9aa5FFAe71612a68B6b8B2eFC4) (impl) - L2ERC20ExtendedTokensBridge: [`0xb4a0Cc7bE277DC9F9CBB6fbE8574B6f5221018D8`](https://soneium.blockscout.com/address/0xb4a0Cc7bE277DC9F9CBB6fbE8574B6f5221018D8) (proxy) - L2ERC20ExtendedTokensBridge: [`0x3e2DcBe31617577d9CF934A9fb97DdC8FD844fa0`](https://soneium.blockscout.com/address/0x3e2DcBe31617577d9CF934A9fb97DdC8FD844fa0) (impl) - Soneium Governance Bridge Executor: [`0xB0F7894b3740F68eAca6e3792B14d2C2c25eF5D4`](https://soneium.blockscout.com/address/0xB0F7894b3740F68eAca6e3792B14d2C2c25eF5D4) - Polygon PoS: - ERC20Predicate: [`0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf`](https://etherscan.io/address/0x40ec5B33f54e0E8A33A975908C5BA1c14e5BbbDf) (proxy) - ERC20Predicate: [`0x1F4c1E0aFbeB5B5b86D7722549274434B29884F6`](https://etherscan.io/address/0x1F4c1E0aFbeB5B5b86D7722549274434B29884F6) (impl) - WstETH UChildERC20: [`0x03b54A6e9a984069379fae1a4fC4dBAE93B3bCCD`](https://polygonscan.com/address/0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd) (proxy) - WstETH UChildERC20: [`0x60991ccaE8f1420B43bf14937a2c9F69162BE21A`](https://polygonscan.com/address/0x60991ccaE8f1420B43bf14937a2c9F69162BE21A) (impl) - Lisk: - L1ERC20TokenBridge: [`0x9348AF23B01F2B517AFE8f29B3183d2Bb7d69Fcf`](https://etherscan.io/address/0x9348AF23B01F2B517AFE8f29B3183d2Bb7d69Fcf) (proxy) - L1ERC20TokenBridge: [`0xC7315f4FaaB2F700fc6b4704BB801c46ff6327AC`](https://etherscan.io/address/0xC7315f4FaaB2F700fc6b4704BB801c46ff6327AC) (impl) - WstETH ERC20Bridged: [`0x76D8de471F54aAA87784119c60Df1bbFc852C415`](https://blockscout.lisk.com/address/0x76D8de471F54aAA87784119c60Df1bbFc852C415) (proxy) - WstETH ERC20Bridged: [`0x16B8006b49db9022BF5457BD2de0144a7d0F970b`](https://blockscout.lisk.com/address/0x16B8006b49db9022BF5457BD2de0144a7d0F970b) (impl) - L2ERC20TokenBridge: [`0xca498Ee83eD3546321d4DC25e2789B0624F15f68`](https://blockscout.lisk.com/address/0xca498Ee83eD3546321d4DC25e2789B0624F15f68) (proxy) - L2ERC20TokenBridge: [`0xE766BE7B76E3F4d06551CB169Dd69B10a58ba91D`](https://blockscout.lisk.com/address/0xE766BE7B76E3F4d06551CB169Dd69B10a58ba91D) (impl) - Lisk Governance Bridge Executor: [`0xfD050cDa025f6378e54ab5fd5Da377D242Ed74d3`](https://blockscout.lisk.com/address/0xfD050cDa025f6378e54ab5fd5Da377D242Ed74d3) --- # Holeลกky :::warning The **Holeลกky** deployment is now fully **deprecated**. Please use the [**Hoodi**](/deployed-contracts/hoodi.md) deployment instead. ::: ## Core protocol - Lido Locator: [`0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8`](https://holesky.etherscan.io/address/0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8) (proxy) - Lido and stETH token: [`0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034`](https://holesky.etherscan.io/address/0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034) (proxy) - wstETH token: [`0x8d09a4502Cc8Cf1547aD300E066060D043f6982D`](https://holesky.etherscan.io/address/0x8d09a4502Cc8Cf1547aD300E066060D043f6982D) - EIP-712 helper for stETH: [`0xE154732c5Eab277fd88a9fF6Bdff7805eD97BCB1`](https://holesky.etherscan.io/address/0xE154732c5Eab277fd88a9fF6Bdff7805eD97BCB1) - StakingRouter: [`0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229`](https://holesky.etherscan.io/address/0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229) (proxy) - Node Operators registry: [`0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC`](https://holesky.etherscan.io/address/0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC) (proxy) - Simple DVT: [`0x11a93807078f8BB880c1BD0ee4C387537de4b4b6`](https://holesky.etherscan.io/address/0x11a93807078f8BB880c1BD0ee4C387537de4b4b6) (proxy) - Deposit Security Module: [`0x808DE3b26Be9438F12E9B45528955EA94C17f217`](https://holesky.etherscan.io/address/0x808DE3b26Be9438F12E9B45528955EA94C17f217) - Execution Layer Rewards Vault: [`0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8`](https://holesky.etherscan.io/address/0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8) - Withdrawal Queue ERC721: [`0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50`](https://holesky.etherscan.io/address/0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50) (proxy) - Withdrawal Vault: [`0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9`](https://holesky.etherscan.io/address/0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9) (proxy) - Burner: [`0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA`](https://holesky.etherscan.io/address/0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA) - MEV Boost Relay Allowed List: [`0x2d86C5855581194a386941806E38cA119E50aEA3`](https://holesky.etherscan.io/address/0x2d86C5855581194a386941806E38cA119E50aEA3) ## Oracle Contracts - Accounting Oracle: - AccountingOracle: [`0x4E97A3972ce8511D87F334dA17a2C332542a5246`](https://holesky.etherscan.io/address/0x4E97A3972ce8511D87F334dA17a2C332542a5246) (proxy) - HashConsensus: [`0xa067FC95c22D51c3bC35fd4BE37414Ee8cc890d2`](https://holesky.etherscan.io/address/0xa067FC95c22D51c3bC35fd4BE37414Ee8cc890d2) - Validators Exit Bus Oracle: - ValidatorsExitBusOracle: [`0xffDDF7025410412deaa05E3E1cE68FE53208afcb`](https://holesky.etherscan.io/address/0xffDDF7025410412deaa05E3E1cE68FE53208afcb) (proxy) - HashConsensus: [`0xe77Cf1A027d7C10Ee6bb7Ede5E922a181FF40E8f`](https://holesky.etherscan.io/address/0xe77Cf1A027d7C10Ee6bb7Ede5E922a181FF40E8f) - OracleReportSanityChecker: [`0x80D1B1fF6E84134404abA18A628347960c38ccA7`](https://holesky.etherscan.io/address/0x80D1B1fF6E84134404abA18A628347960c38ccA7) - OracleDaemonConfig: [`0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7`](https://holesky.etherscan.io/address/0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7) ## DAO contracts - Lido DAO (Kernel): [`0x3b03f75Ec541Ca11a223bB58621A3146246E1644`](https://holesky.etherscan.io/address/0x3b03f75Ec541Ca11a223bB58621A3146246E1644) (proxy) - LDO token: [`0x14ae7daeecdf57034f3E9db8564e46Dba8D97344`](https://holesky.etherscan.io/address/0x14ae7daeecdf57034f3E9db8564e46Dba8D97344) - Aragon Voting: [`0xdA7d2573Df555002503F29aA4003e398d28cc00f`](https://holesky.etherscan.io/address/0xdA7d2573Df555002503F29aA4003e398d28cc00f) (proxy) - Aragon Token Manager: [`0xFaa1692c6eea8eeF534e7819749aD93a1420379A`](https://holesky.etherscan.io/address/0xFaa1692c6eea8eeF534e7819749aD93a1420379A) (proxy) - Aragon Finance: [`0xf0F281E5d7FBc54EAFcE0dA225CDbde04173AB16`](https://holesky.etherscan.io/address/0xf0F281E5d7FBc54EAFcE0dA225CDbde04173AB16) (proxy) - Aragon Agent: [`0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d`](https://holesky.etherscan.io/address/0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d) (proxy) - Aragon ACL: [`0xfd1E42595CeC3E83239bf8dFc535250e7F48E0bC`](https://holesky.etherscan.io/address/0xfd1E42595CeC3E83239bf8dFc535250e7F48E0bC) (proxy) - Voting Repo: [`0x2997EA0D07D79038D83Cb04b3BB9A2Bc512E3fDA`](https://holesky.etherscan.io/address/0x2997EA0D07D79038D83Cb04b3BB9A2Bc512E3fDA) (proxy) - Token Manager Repo: [`0xD327b4Fb87fa01599DaD491Aa63B333c44C74472`](https://holesky.etherscan.io/address/0xD327b4Fb87fa01599DaD491Aa63B333c44C74472) (proxy) - Finance Repo: [`0x0df65b7c78Dc42a872010d031D3601C284D8fE71`](https://holesky.etherscan.io/address/0x0df65b7c78Dc42a872010d031D3601C284D8fE71) (proxy) - Agent Repo: [`0xe7b4567913AaF2bD54A26E742cec22727D8109eA`](https://holesky.etherscan.io/address/0xe7b4567913AaF2bD54A26E742cec22727D8109eA) (proxy) - Lido App Repo: [`0xA37fb4C41e7D30af5172618a863BBB0f9042c604`](https://holesky.etherscan.io/address/0xA37fb4C41e7D30af5172618a863BBB0f9042c604) (proxy) - Node Operators Registry Repo: [`0x4E8970d148CB38460bE9b6ddaab20aE2A74879AF`](https://holesky.etherscan.io/address/0x4E8970d148CB38460bE9b6ddaab20aE2A74879AF) (proxy) - Simple DVT Repo: [`0x889dB59baf032E1dfD4fCA720e0833c24f1404C6`](https://holesky.etherscan.io/address/0x889dB59baf032E1dfD4fCA720e0833c24f1404C6) (proxy) - EVMScriptRegistry: [`0xE1200ae048163B67D69Bc0492bF5FddC3a2899C0`](https://holesky.etherscan.io/address/0xE1200ae048163B67D69Bc0492bF5FddC3a2899C0) (proxy) - CallsScript: [`0xAa8B4F258a4817bfb0058b861447878168ddf7B0`](https://holesky.etherscan.io/address/0xAa8B4F258a4817bfb0058b861447878168ddf7B0) - Lido APMRegistry: [`0x4605Dc9dC4BD0442F850eB8226B94Dd0e27C3Ce7`](https://holesky.etherscan.io/address/0x4605Dc9dC4BD0442F850eB8226B94Dd0e27C3Ce7) (proxy) - Aragon APMRegistry: [`0xB576A85c310CC7Af5C106ab26d2942fA3a5ea94A`](https://holesky.etherscan.io/address/0xB576A85c310CC7Af5C106ab26d2942fA3a5ea94A) (proxy) - GateSeal Blueprint: [`0x2e4fc708A6073241b6884dC72D817c6eb2632229`](https://holesky.etherscan.io/address/0x2e4fc708A6073241b6884dC72D817c6eb2632229) - GateSeal Factory: [`0x1134F7077055b0B3559BE52AfeF9aA22A0E1eEC2`](https://holesky.etherscan.io/address/0x1134F7077055b0B3559BE52AfeF9aA22A0E1eEC2) - GateSeal: [`0xAE6eCd77DCC656c5533c4209454Fd56fB46e1778`](https://holesky.etherscan.io/address/0xAE6eCd77DCC656c5533c4209454Fd56fB46e1778) ### Dual Governance - Emergency Protected Timelock: [`0xe9c5FfEAd0668AFdBB9aac16163840d649DB76DD`](https://holesky.etherscan.io/address/0xe9c5FfEAd0668AFdBB9aac16163840d649DB76DD) - Admin Executor: [`0x8BD0a916faDa88Ba3accb595a3Acd28F467130e8`](https://holesky.etherscan.io/address/0x8BD0a916faDa88Ba3accb595a3Acd28F467130e8) - Dual Governance: [`0x490bf377734CA134A8E207525E8576745652212e`](https://holesky.etherscan.io/address/0x490bf377734CA134A8E207525E8576745652212e) - Dual Governance Config Provider: [`0xF3257b7E333Cdd15df92CBc3BAF645D83D22B97B`](https://holesky.etherscan.io/address/0xF3257b7E333Cdd15df92CBc3BAF645D83D22B97B) - Emergency Timelocked Governance: [`0x46c6C7E1Cc438456d658Eed61A764a475abDa0C1`](https://holesky.etherscan.io/address/0x46c6C7E1Cc438456d658Eed61A764a475abDa0C1) - Escrow: [`0x901bd16E9B8c317891E3b7D3D57f98Da50De5a36`](https://holesky.etherscan.io/address/0x901bd16E9B8c317891E3b7D3D57f98Da50De5a36) (impl) - Reseal Manager: [`0x9dE2273f9f1e81145171CcA927EFeE7aCC64c9fb`](https://holesky.etherscan.io/address/0x9dE2273f9f1e81145171CcA927EFeE7aCC64c9fb) - Tiebreaker Core Committee: [`0xE449EEd4C99EcC0157690f84cE64A6d66a83af55`](https://holesky.etherscan.io/address/0xE449EEd4C99EcC0157690f84cE64A6d66a83af55) - Tiebreaker Sub Committees: - Developers Sub Committee 1 [`0xa0E6A8810E49b8A509dd01659d4A6D1EC0bBbA27`](https://holesky.etherscan.io/address/0xa0E6A8810E49b8A509dd01659d4A6D1EC0bBbA27) - Developers Sub Committee 2 [`0xbfD6b8f44fcf65B5809d6B71FDc52c21Bcb4D13F`](https://holesky.etherscan.io/address/0xbfD6b8f44fcf65B5809d6B71FDc52c21Bcb4D13F) - Developers Sub Committee 3 [`0xf822A746aA1ACC0b68649Ac83A9A93651B98B1b0`](https://holesky.etherscan.io/address/0xf822A746aA1ACC0b68649Ac83A9A93651B98B1b0) ## Data Bus - DataBus on Chiado (Testnet): [`0x37De961D6bb5865867aDd416be07189D2Dd960e6`](https://gnosis-chiado.blockscout.com/address/0x37De961D6bb5865867aDd416be07189D2Dd960e6) ## Staking modules ### Curated Module - Node Operators Registry: [`0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC`](https://holesky.etherscan.io/address/0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC) ### Simple DVT Module - Node Operators Registry: [`0x11a93807078f8BB880c1BD0ee4C387537de4b4b6`](https://holesky.etherscan.io/address/0x11a93807078f8BB880c1BD0ee4C387537de4b4b6) ### Sandbox Module - Node Operators Registry: [`0xD6C2ce3BB8bea2832496Ac8b5144819719f343AC`](https://holesky.etherscan.io/address/0xD6C2ce3BB8bea2832496Ac8b5144819719f343AC) ### Community Staking Module - CSModule: [`0x4562c3e63c2e586cD1651B958C22F88135aCAd4f`](https://holesky.etherscan.io/address/0x4562c3e63c2e586cD1651B958C22F88135aCAd4f) (proxy) - CSAccounting: [`0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1`](https://holesky.etherscan.io/address/0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1) (proxy) - CSFeeDistributor: [`0xD7ba648C8F72669C6aE649648B516ec03D07c8ED`](https://holesky.etherscan.io/address/0xD7ba648C8F72669C6aE649648B516ec03D07c8ED) (proxy) - CSFeeOracle: [`0xaF57326C7d513085051b50912D51809ECC5d98Ee`](https://holesky.etherscan.io/address/0xaF57326C7d513085051b50912D51809ECC5d98Ee) (proxy) - CSVerifier: [`0xc099dfd61f6e5420e0ca7e84d820daad17fc1d44`](https://holesky.etherscan.io/address/0xc099dfd61f6e5420e0ca7e84d820daad17fc1d44) - CSEarlyAdoption: [`0x71E92eA77C198a770d9f33A03277DbeB99989660`](https://holesky.etherscan.io/address/0x71E92eA77C198a770d9f33A03277DbeB99989660) - HashConsensus: [`0xbF38618Ea09B503c1dED867156A0ea276Ca1AE37`](https://holesky.etherscan.io/address/0xbF38618Ea09B503c1dED867156A0ea276Ca1AE37) - GateSeal: [`0xf1C03536dbC77B1bD493a2D1C0b1831Ea78B540a`](https://holesky.etherscan.io/address/0xf1C03536dbC77B1bD493a2D1C0b1831Ea78B540a) ## DAO Ops contracts & addresses - Token Reward Program (TRP) VestingEscrowFactory: [`0x586f0b51d46ac8ac6058702d99cd066ae514e96b`](https://holesky.etherscan.io/address/0x586f0b51d46ac8ac6058702d99cd066ae514e96b) ## Easy Track - EasyTrack: [`0x1763b9ED3586B08AE796c7787811a2E1bc16163a`](https://holesky.etherscan.io/address/0x1763b9ED3586B08AE796c7787811a2E1bc16163a) - EVMScriptExecutor: [`0x2819B65021E13CEEB9AC33E77DB32c7e64e7520D`](https://holesky.etherscan.io/address/0x2819B65021E13CEEB9AC33E77DB32c7e64e7520D) ### Easy Track factories for staking modules - **Curated Node Operators staking module** (registry: [`0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC`](https://holesky.etherscan.io/address/0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC)) - IncreaseNodeOperatorStakingLimit: [`0x18Ff3bD97739bf910cDCDb8d138976c6afDB4449`](https://holesky.etherscan.io/address/0x18Ff3bD97739bf910cDCDb8d138976c6afDB4449) - **Simple DVT staking module** (registry: [`0x11a93807078f8BB880c1BD0ee4C387537de4b4b6`](https://holesky.etherscan.io/address/0x11a93807078f8BB880c1BD0ee4C387537de4b4b6), committee ms [`0xD76001b33b23452243E2FDa833B6e7B8E3D43198`](https://holesky.etherscan.io/address/0xD76001b33b23452243E2FDa833B6e7B8E3D43198)) - AddNodeOperators: [`0xeF5233A5bbF243149E35B353A73FFa8931FDA02b`](https://holesky.etherscan.io/address/0xeF5233A5bbF243149E35B353A73FFa8931FDA02b) - ActivateNodeOperators: [`0x5b4A9048176D5bA182ceec8e673D8aA6927A40D6`](https://holesky.etherscan.io/address/0x5b4A9048176D5bA182ceec8e673D8aA6927A40D6) - DeactivateNodeOperators: [`0x88d247cdf4ff4A4AAA8B3DD9dd22D1b89219FB3B`](https://holesky.etherscan.io/address/0x88d247cdf4ff4A4AAA8B3DD9dd22D1b89219FB3B) - SetVettedValidatorsLimits: [`0x30Cb36DBb0596aD9Cf5159BD2c4B1456c18e47E8`](https://holesky.etherscan.io/address/0x30Cb36DBb0596aD9Cf5159BD2c4B1456c18e47E8) - SetNodeOperatorNames: [`0x4792BaC0a262200fA7d3b68e7622bFc1c2c3a72d`](https://holesky.etherscan.io/address/0x4792BaC0a262200fA7d3b68e7622bFc1c2c3a72d) - SetNodeOperatorRewardAddresses: [`0x6Bfc576018C7f3D2a9180974E5c8e6CFa021f617`](https://holesky.etherscan.io/address/0x6Bfc576018C7f3D2a9180974E5c8e6CFa021f617) - UpdateTargetValidatorLimits: [`0x431a156BEba95803a95452441C1959c4479710e1`](https://holesky.etherscan.io/address/0x431a156BEba95803a95452441C1959c4479710e1) - ChangeNodeOperatorManager: [`0xb8C4728bc0826bA5864D02FA53148de7A44C2f7E`](https://holesky.etherscan.io/address/0xb8C4728bc0826bA5864D02FA53148de7A44C2f7E) - **Sandbox staking module** (registry: [`0xD6C2ce3BB8bea2832496Ac8b5144819719f343AC`](https://holesky.etherscan.io/address/0xD6C2ce3BB8bea2832496Ac8b5144819719f343AC)) - IncreaseNodeOperatorStakingLimit: [`0xbD37e55748c6f4Ece637AeD3e278e7575346B587`](https://holesky.etherscan.io/address/0xbD37e55748c6f4Ece637AeD3e278e7575346B587) - **Community Staking Module** (module: [`0x4562c3e63c2e586cD1651B958C22F88135aCAd4f`](https://holesky.etherscan.io/address/0x4562c3e63c2e586cD1651B958C22F88135aCAd4f)) - CSMSettleElStealingPenalty: [`0x07696EA8A5b53C3E35d9cce10cc62c6c79C4691D`](https://holesky.etherscan.io/address/0x07696EA8A5b53C3E35d9cce10cc62c6c79C4691D) ### Easy Track factories for token transfers - **LOL (ex.reWARDS) stETH** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x55B304a585D540421F1fD3579Ef12Abab7304492`](https://holesky.etherscan.io/address/0x55B304a585D540421F1fD3579Ef12Abab7304492) - AddAllowedRecipient: [`0xf0968B9bE18282dD23bbbC79a1c9C8996CE6984D`](https://holesky.etherscan.io/address/0xf0968B9bE18282dD23bbbC79a1c9C8996CE6984D) - RemoveAllowedRecipient: [`0xF0F34b82241cD49BB3952149BD30A08Eb9D8B54E`](https://holesky.etherscan.io/address/0xF0F34b82241cD49BB3952149BD30A08Eb9D8B54E) - TopUpAllowedRecipients: [`0xBB06DD9a3C7eE8cE093860094e769a1E3D6F97F6`](https://holesky.etherscan.io/address/0xBB06DD9a3C7eE8cE093860094e769a1E3D6F97F6) - **Rewards Share stETH** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0xAc2F596191c75B77c2835Afe83c3a9097f0AC071`](https://holesky.etherscan.io/address/0xAc2F596191c75B77c2835Afe83c3a9097f0AC071) - AddAllowedRecipient: [`0x49D3211203e8E18B4e60F74C1126934da2520987`](https://holesky.etherscan.io/address/0x49D3211203e8E18B4e60F74C1126934da2520987) - RemoveAllowedRecipient: [`0x112c48c4659A9a1d42a3e45EBc8e37B6150F2B0C`](https://holesky.etherscan.io/address/0x112c48c4659A9a1d42a3e45EBc8e37B6150F2B0C) - TopUpAllowedRecipients: [`0x089bc04630c056D76fF4Ec172e752A7d5B855e16`](https://holesky.etherscan.io/address/0x089bc04630c056D76fF4Ec172e752A7d5B855e16) - **LEGO LDO** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x77CF728329920E4191a6Edd9b009cD055D3cD29A`](https://holesky.etherscan.io/address/0x77CF728329920E4191a6Edd9b009cD055D3cD29A) - TopUpAllowedRecipients: [`0xCfaFcD35ACcc4383e2CCDf7DD3F58114914F1955`](https://holesky.etherscan.io/address/0xCfaFcD35ACcc4383e2CCDf7DD3F58114914F1955) - **LEGO stablecoins** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x10Ff9c02C65775379D9E20BFF9AC92Cbaf15Ab8F`](https://holesky.etherscan.io/address/0x10Ff9c02C65775379D9E20BFF9AC92Cbaf15Ab8F) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0x7Bb5C5965a63aFb6a05D19bB03e3f170E2d7d684`](https://holesky.etherscan.io/address/0x7Bb5C5965a63aFb6a05D19bB03e3f170E2d7d684) - **TRP LDO** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x5f4E9A917d6556dB91Cf351f49b0edCc5A255bAE`](https://holesky.etherscan.io/address/0x5f4E9A917d6556dB91Cf351f49b0edCc5A255bAE) - TopUpAllowedRecipients: [`0xD618F0CF48F057B5256e102dC18d8011e08c19D3`](https://holesky.etherscan.io/address/0xD618F0CF48F057B5256e102dC18d8011e08c19D3) - **Gas Supply stETH** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x1B68a7BeE396e2eaAD9D2716E0A271A4BB568BCd`](https://holesky.etherscan.io/address/0x1B68a7BeE396e2eaAD9D2716E0A271A4BB568BCd) - AddAllowedRecipient: [`0x13dB9E1ddE54d2641f571EA288D9e79C0E8bce2e`](https://holesky.etherscan.io/address/0x13dB9E1ddE54d2641f571EA288D9e79C0E8bce2e) - RemoveAllowedRecipient: [`0x64CE36D2DC7e7786BF56D2DF8A5F3c788977Fb19`](https://holesky.etherscan.io/address/0x64CE36D2DC7e7786BF56D2DF8A5F3c788977Fb19) - TopUpAllowedRecipients: [`0xf97E048A952d170d5D5E817C8D9c8253f4D50F96`](https://holesky.etherscan.io/address/0xf97E048A952d170d5D5E817C8D9c8253f4D50F96) - **RCC stablecoins** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x17Ab17290bcDbea381500525A58e16e29093523c`](https://holesky.etherscan.io/address/0x17Ab17290bcDbea381500525A58e16e29093523c) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0xD497E7e039FeFBc64dBB7b75368afb06D07Bc73F`](https://holesky.etherscan.io/address/0xD497E7e039FeFBc64dBB7b75368afb06D07Bc73F) - **RCC stETH** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x916B909300c4aB5ADC4247cebd840C9278683e78`](https://holesky.etherscan.io/address/0x916B909300c4aB5ADC4247cebd840C9278683e78) - TopUpAllowedRecipients: [`0xe3bCa174A8b031C61a58aa56a0f622D4FFCA47d7`](https://holesky.etherscan.io/address/0xe3bCa174A8b031C61a58aa56a0f622D4FFCA47d7) - **PML stablecoins** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x580B23a97F827F2b6E51B3DEc270Ef522Ccf520c`](https://holesky.etherscan.io/address/0x580B23a97F827F2b6E51B3DEc270Ef522Ccf520c) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0x5BAE56ECfB616eAbbDB048AC930FA1Db82f18900`](https://holesky.etherscan.io/address/0x5BAE56ECfB616eAbbDB048AC930FA1Db82f18900) - **PML stETH** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0xC2Ec8a9285D111de54725FAD1AC6a3B7E3BC6225`](https://holesky.etherscan.io/address/0xC2Ec8a9285D111de54725FAD1AC6a3B7E3BC6225) - TopUpAllowedRecipients: [`0x8612A51e4914FfFb25D96d1A310D4C6342c2091E`](https://holesky.etherscan.io/address/0x8612A51e4914FfFb25D96d1A310D4C6342c2091E) - **ATC stablecoins** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x37675423796D39C19351c5C322C3692b23a3d9bd`](https://holesky.etherscan.io/address/0x37675423796D39C19351c5C322C3692b23a3d9bd) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0xfa54cf78474cD4A7f4408Dd0efA36e44b6269813`](https://holesky.etherscan.io/address/0xfa54cf78474cD4A7f4408Dd0efA36e44b6269813) - **ATC stETH** (committee ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x955bA61676dAd6091Ff3F9BC498219D6DbD49107`](https://holesky.etherscan.io/address/0x955bA61676dAd6091Ff3F9BC498219D6DbD49107) - TopUpAllowedRecipients: [`0x1395970895282333dC914172944f52F15Df63620`](https://holesky.etherscan.io/address/0x1395970895282333dC914172944f52F15Df63620) - **Alliance Ops stablecoins** (trusted caller is QA & DAO Ops ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://holesky.etherscan.io/address/0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0xe1ba8dee84a4df8e99e495419365d979cdb19991`](https://holesky.etherscan.io/address/0xe1ba8dee84a4df8e99e495419365d979cdb19991) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0x343fa5f0c79277e2d27e440f40420d619f962a23`](https://holesky.etherscan.io/address/0x343fa5f0c79277e2d27e440f40420d619f962a23) - **Sandbox stablecoins** (trusted caller is DAO Ops testnet EOA [`0xd4090ca1134f8de1450b8246916f73d212efdef6`](https://holesky.etherscan.io/address/0xd4090ca1134f8de1450b8246916f73d212efdef6)) - AllowedRecipientsRegistry: [`0xF8a63a36B954D72de197097377aa00C238c653Cf`](https://holesky.etherscan.io/address/0xF8a63a36B954D72de197097377aa00C238c653Cf) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0x71bcEf1f4E4945005e1D22d68F02085D5167ab43`](https://holesky.etherscan.io/address/0x71bcEf1f4E4945005e1D22d68F02085D5167ab43) - AddAllowedRecipient: [`0xB238fB1e7c8da5da022140dA956Fc3052808fC56`](https://holesky.etherscan.io/address/0xB238fB1e7c8da5da022140dA956Fc3052808fC56) - RemoveAllowedRecipient: [`0x51c730af05777c4D3CcC8c8B80558F4D155bb7BF`](https://holesky.etherscan.io/address/0x51c730af05777c4D3CcC8c8B80558F4D155bb7BF) - **Ecosystem BORG Foundation operational funds stablecoins** (trusted caller is QA & DAO Ops ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://holesky.etherscan.io/address/0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x0214CEBDEc06dc2729382860603d01113F068388`](https://holesky.etherscan.io/address/0x0214CEBDEc06dc2729382860603d01113F068388) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0x167caEDde0F3230eB18763270B11c970409F389e`](https://holesky.etherscan.io/address/0x167caEDde0F3230eB18763270B11c970409F389e) - **Labs BORG Foundation operational funds stablecoins** (trusted caller is QA & DAO Ops ms [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://holesky.etherscan.io/address/0x96d2Ff1C4D30f592B91fd731E218247689a76915)) - AllowedRecipientsRegistry: [`0x303F5b60e3cf6Ea11d8509A1546401e311A13B92`](https://holesky.etherscan.io/address/0x303F5b60e3cf6Ea11d8509A1546401e311A13B92) - AllowedTokensRegistry: [`0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666`](https://holesky.etherscan.io/address/0x091c0ec8b4d54a9fcb36269b5d5e5af43309e666) - TopUpAllowedRecipients: [`0xf7304738E9d4F572b909FaEd32504F558E234cdB`](https://holesky.etherscan.io/address/0xf7304738E9d4F572b909FaEd32504F558E234cdB) - **Tooling contracts:** - AllowedRecipientsBuilder (single token): [`0xeC3785b13b21c226D66B5bC2E82BB2f4226f715e`](https://holesky.etherscan.io/address/0xeC3785b13b21c226D66B5bC2E82BB2f4226f715e) - AllowedRecipientsFactory (single token): [`0x62a65b0eC11D74a00FcC445ef7A3374f02635Dd9`](https://holesky.etherscan.io/address/0x62a65b0eC11D74a00FcC445ef7A3374f02635Dd9) - AllowedRecipientsBuilder (multi token): [`0x983dF2EA3A7Dce9D60bD06f5C5dCc44a138eBA89`](https://holesky.etherscan.io/address/0x983dF2EA3A7Dce9D60bD06f5C5dCc44a138eBA89) - AllowedRecipientsFactory (multi token): [`0x9d156F7F5ed1fEbDc4996CAA835CD964A10bd650`](https://holesky.etherscan.io/address/0x9d156F7F5ed1fEbDc4996CAA835CD964A10bd650) - BokkyPooBah's DateTime Library: [`0xd6237FecDF9C1D9b023A5205C17549E3037EeEec`](https://holesky.etherscan.io/address/0xd6237FecDF9C1D9b023A5205C17549E3037EeEec) ## Testnet DAO Multisigs & EOA - QA & DAO Ops ms: [`0x96d2Ff1C4D30f592B91fd731E218247689a76915`](https://stg.holesky-safe.protofire.io/home?safe=holesky:0x96d2Ff1C4D30f592B91fd731E218247689a76915) - QA testnet EOA: [`0x1580881349e214Bab9f1E533bF97351271DB95a9`](https://holesky.etherscan.io/address/0x1580881349e214Bab9f1E533bF97351271DB95a9) - DAO Ops testnet EOA: [`0xd4090CA1134F8dE1450B8246916F73d212efdEf6`](https://holesky.etherscan.io/address/0xd4090CA1134F8dE1450B8246916F73d212efdEf6) - Testnet GateSeal Committee: [`0x6165267E76D609465640bffc158aff7905D47B46`](https://holesky-safe.protofire.io/home?safe=holesky:0x6165267E76D609465640bffc158aff7905D47B46) \[[proposed to rename to CircuitBreaker Committee](https://research.lido.fi/t/circuitbreaker-programmable-panic-layer/11400#p-24944-proposed-committees-6)\] - CSM Development Team EOA: [`0xc4DAB3a3ef68C6DFd8614a870D64D475bA44F164`](https://holesky.etherscan.io/address/0xc4DAB3a3ef68C6DFd8614a870D64D475bA44F164) ## Testnet Stablecoins - USDC: [`0x9715b2786f1053294fc8952df923b95cab9aac42`](https://holesky.etherscan.io/address/0x9715b2786f1053294fc8952df923b95cab9aac42) - USDT: [`0x86f6c353a0965eb069cd7f4f91c1afef8c725551`](https://holesky.etherscan.io/address/0x86f6c353a0965eb069cd7f4f91c1afef8c725551) - DAI: [`0x2eb8e9198e647f80ccf62a5e291bcd4a5a3ca68c`](https://holesky.etherscan.io/address/0x2eb8e9198e647f80ccf62a5e291bcd4a5a3ca68c) --- # Hoodi :::info **Primary Lido Protocol Testnet** Hoodi is the primary operational and actively maintained Lido protocol testnet. This page lists the contract addresses for the testnet, including all deployed protocol components and extensions used for testing. **Deployment Information:** - โš“ Lido protocol version: [**`v4.0.1`**](https://github.com/lidofinance/core/releases/tag/v4.0.1) - ๐ŸŒ Network: Ethereum Hoodi (Chain ID: `560048`) - โœ… Status: Active and maintained ::: ## ๐Ÿ›๏ธ Core Protocol {#core-protocol} - Lido Locator: [`0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8`](https://hoodi.etherscan.io/address/0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8) (proxy) - Lido Locator: [`0x546d76dd8D4BC0c6a26Cb71a39De5d78E222Cbf8`](https://hoodi.etherscan.io/address/0x546d76dd8D4BC0c6a26Cb71a39De5d78E222Cbf8) (impl) - Lido and stETH token: [`0x3508A952176b3c15387C97BE809eaffB1982176a`](https://hoodi.etherscan.io/address/0x3508A952176b3c15387C97BE809eaffB1982176a) (proxy) - Lido and stETH token: [`0xB9A2Fb8336f3775d790b3FdD6151e3F193AA7352`](https://hoodi.etherscan.io/address/0xB9A2Fb8336f3775d790b3FdD6151e3F193AA7352) (impl) - wstETH token: [`0x7E99eE3C66636DE415D2d7C880938F2f40f94De4`](https://hoodi.etherscan.io/address/0x7E99eE3C66636DE415D2d7C880938F2f40f94De4) - wstETH referral staker: [`0xf886BcC68b240316103fE8A12453Ce7831c2e835`](https://hoodi.etherscan.io/address/0xf886BcC68b240316103fE8A12453Ce7831c2e835) - EIP-712 helper for stETH: [`0x2A1d51BF3aAA7A7D027C8f561e5f579876a17B0a`](https://hoodi.etherscan.io/address/0x2A1d51BF3aAA7A7D027C8f561e5f579876a17B0a) - Staking Router: [`0xCc820558B39ee15C7C45B59390B503b83fb499A8`](https://hoodi.etherscan.io/address/0xCc820558B39ee15C7C45B59390B503b83fb499A8) (proxy) - Staking Router: [`0x05C392877165372Bf76dd08d52D4445bFEd6FF1F`](https://hoodi.etherscan.io/address/0x05C392877165372Bf76dd08d52D4445bFEd6FF1F) (impl) - Deposit Security Module: [`0x8E63F0aF403ffd3Cbd5dB18b4ee632314ab49B51`](https://hoodi.etherscan.io/address/0x8E63F0aF403ffd3Cbd5dB18b4ee632314ab49B51) - TopUp Gateway: [`0x10DBEb3367876826d00D21718D1d893e0fbD2956`](https://hoodi.etherscan.io/address/0x10DBEb3367876826d00D21718D1d893e0fbD2956) (proxy) - TopUp Gateway: [`0x8621D8a402fdf2a131E38e16ac50f4C97660Fc2b`](https://hoodi.etherscan.io/address/0x8621D8a402fdf2a131E38e16ac50f4C97660Fc2b) (impl) - Execution Layer Rewards Vault: [`0x9b108015fe433F173696Af3Aa0CF7CDb3E104258`](https://hoodi.etherscan.io/address/0x9b108015fe433F173696Af3Aa0CF7CDb3E104258) - Withdrawal Queue ERC721: [`0xfe56573178f1bcdf53F01A6E9977670dcBBD9186`](https://hoodi.etherscan.io/address/0xfe56573178f1bcdf53F01A6E9977670dcBBD9186) (proxy) - Withdrawal Vault: [`0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2`](https://hoodi.etherscan.io/address/0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2) (proxy) - Withdrawal Vault: [`0x6724DaD16f7b05157dF72783F99cA6B813742330`](https://hoodi.etherscan.io/address/0x6724DaD16f7b05157dF72783F99cA6B813742330) (impl) - Accounting: [`0x9b5b78D1C9A3238bF24662067e34c57c83E8c354`](https://hoodi.etherscan.io/address/0x9b5b78D1C9A3238bF24662067e34c57c83E8c354) (proxy) - Accounting: [`0xA39fe063A6d1420E59d218d318d52D84Fbd9202F`](https://hoodi.etherscan.io/address/0xA39fe063A6d1420E59d218d318d52D84Fbd9202F) (impl) - Burner: [`0xb2c99cd38a2636a6281a849C8de938B3eF4A7C3D`](https://hoodi.etherscan.io/address/0xb2c99cd38a2636a6281a849C8de938B3eF4A7C3D) (proxy) - Beacon Chain Depositor: [`0x963ea462c33684Df516405B4f59c718EE4E22932`](https://hoodi.etherscan.io/address/0x963ea462c33684Df516405B4f59c718EE4E22932) (external lib) - Min First Allocation Strategy: [`0xB2A07615cDe70Dc99f493aA5556175405537B21b`](https://hoodi.etherscan.io/address/0xB2A07615cDe70Dc99f493aA5556175405537B21b) (external lib) - SRLib: [`0x7c178B9B797C6ea6776A784C22A0f95a79385c9b`](https://hoodi.etherscan.io/address/0x7c178B9B797C6ea6776A784C22A0f95a79385c9b) (external lib) - MEV Boost Relay Allowed List: [`0x279d3A456212a1294DaEd0faEE98675a52E8A4Bf`](https://hoodi.etherscan.io/address/0x279d3A456212a1294DaEd0faEE98675a52E8A4Bf) - Triggerable Withdrawals Gateway: [`0x6679090D92b08a2a686eF8614feECD8cDFE209db`](https://hoodi.etherscan.io/address/0x6679090D92b08a2a686eF8614feECD8cDFE209db) - Validator Exit Delay Verifier: [`0xa5F5A9360275390fF9728262a29384399f38d2f0`](https://hoodi.etherscan.io/address/0xa5F5A9360275390fF9728262a29384399f38d2f0) - Vault Hub: [`0x4C9fFC325392090F789255b9948Ab1659b797964`](https://hoodi.etherscan.io/address/0x4C9fFC325392090F789255b9948Ab1659b797964) (proxy) - Vault Hub: [`0xAd2C869FE66Ff4c0E347A0824Af92D2B7C91288A`](https://hoodi.etherscan.io/address/0xAd2C869FE66Ff4c0E347A0824Af92D2B7C91288A) (impl) - Predeposit Guarantee: [`0xa5F55f3402beA2B14AE15Dae1b6811457D43581d`](https://hoodi.etherscan.io/address/0xa5F55f3402beA2B14AE15Dae1b6811457D43581d) (proxy) - Operator Grid: [`0x501e678182bB5dF3f733281521D3f3D1aDe69917`](https://hoodi.etherscan.io/address/0x501e678182bB5dF3f733281521D3f3D1aDe69917) (proxy) ### ๐Ÿ”จ stVaults Factory Stack {#stvaults-factory-stack} - Staking Vault Factory: [`0x7Ba269a03eeD86f2f54CB04CA3b4b7626636Df4E`](https://hoodi.etherscan.io/address/0x7Ba269a03eeD86f2f54CB04CA3b4b7626636Df4E) - Staking Vault Beacon: [`0xb3e6a8B6A752d3bb905A1B3Ef12bbdeE77E8160e`](https://hoodi.etherscan.io/address/0xb3e6a8B6A752d3bb905A1B3Ef12bbdeE77E8160e) - Staking Vault Implementation: [`0xE96BE4FB723e68e7b96244b7399C64a58bcD0062`](https://hoodi.etherscan.io/address/0xE96BE4FB723e68e7b96244b7399C64a58bcD0062) - Staking Vault Pinned Beacon Proxy: [`0x3e144aEd003b5AE6953A99B78dD34154CF3F8c76`](https://hoodi.etherscan.io/address/0x3e144aEd003b5AE6953A99B78dD34154CF3F8c76) - Dashboard Implementation: [`0x38131D5548Be57A34937521fe427a23f49e1e2d4`](https://hoodi.etherscan.io/address/0x38131D5548Be57A34937521fe427a23f49e1e2d4) - Validator Consolidation Requests: [`0xbf95Cd394cC03cD03fEA62A435ac347314877f1d`](https://hoodi.etherscan.io/address/0xbf95Cd394cC03cD03fEA62A435ac347314877f1d) ### ๐ŸŒŠ DeFi Wrapper {#defi-wrapper} - DeFi Wrapper Factory: [`0xd05ebF24A340ece8B8FB53a170F1171DCd02b4d9`](https://hoodi.etherscan.io/address/0xd05ebF24A340ece8B8FB53a170F1171DCd02b4d9) - Lido Earn Strategy Factory: [`0x0b860bfFDA72D214Dc8aC98bEcd8D1cd55307561`](https://hoodi.etherscan.io/address/0x0b860bfFDA72D214Dc8aC98bEcd8D1cd55307561) ### ๐Ÿ”— Consolidation Stack {#consolidation-stack} - Consolidation Migrator: [`0x747d357F5b6410B80Eb63406FaC5E2A91131B4f8`](https://hoodi.etherscan.io/address/0x747d357F5b6410B80Eb63406FaC5E2A91131B4f8) (proxy) - Consolidation Migrator: [`0x8BF11ead77fFe142AB27BE486Bc5aB22D9baF520`](https://hoodi.etherscan.io/address/0x8BF11ead77fFe142AB27BE486Bc5aB22D9baF520) (impl) - Consolidation Bus: [`0xe09fBcE63826468867eE66Eda491E444829E022A`](https://hoodi.etherscan.io/address/0xe09fBcE63826468867eE66Eda491E444829E022A) (proxy) - Consolidation Bus: [`0x2908c4B32548D8799fDc601C1a75E7d5C2FbC556`](https://hoodi.etherscan.io/address/0x2908c4B32548D8799fDc601C1a75E7d5C2FbC556) (impl) - Consolidation Gateway: [`0xC9991Bb865d025364BCbBd99f36e85889Fb68e69`](https://hoodi.etherscan.io/address/0xC9991Bb865d025364BCbBd99f36e85889Fb68e69) ## ๐Ÿ”ฎ Oracle Contracts {#oracle-contracts} - Accounting Oracle: - AccountingOracle: [`0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216`](https://hoodi.etherscan.io/address/0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216) (proxy) - AccountingOracle: [`0xD00dD90651f031ED3158Cf75AC6e4361B4CCcBD8`](https://hoodi.etherscan.io/address/0xD00dD90651f031ED3158Cf75AC6e4361B4CCcBD8) (impl) - HashConsensus: [`0x32EC59a78abaca3f91527aeB2008925D5AaC1eFC`](https://hoodi.etherscan.io/address/0x32EC59a78abaca3f91527aeB2008925D5AaC1eFC) - Validators Exit Bus Oracle (Validator Exit Bus): - ValidatorsExitBusOracle: [`0x8664d394C2B3278F26A1B44B967aEf99707eeAB2`](https://hoodi.etherscan.io/address/0x8664d394C2B3278F26A1B44B967aEf99707eeAB2) (proxy) - ValidatorsExitBusOracle: [`0xA881F320E17Aa97d6Ce0bE76B0e2620bd2FC555A`](https://hoodi.etherscan.io/address/0xA881F320E17Aa97d6Ce0bE76B0e2620bd2FC555A) (impl) - HashConsensus: [`0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c`](https://hoodi.etherscan.io/address/0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c) - OracleReportSanityChecker: [`0xD0261b0032A00a7449ee7fbE14d3f98702996441`](https://hoodi.etherscan.io/address/0xD0261b0032A00a7449ee7fbE14d3f98702996441) - OracleDaemonConfig: [`0x2a833402e3F46fFC1ecAb3598c599147a78731a9`](https://hoodi.etherscan.io/address/0x2a833402e3F46fFC1ecAb3598c599147a78731a9) - Lazy Oracle: [`0xf41491C79C30e8f4862d3F4A5b790171adB8e04A`](https://hoodi.etherscan.io/address/0xf41491C79C30e8f4862d3F4A5b790171adB8e04A) (proxy) - Lazy Oracle: [`0xC372aBC601C4eE5aA82CA2bcb54Da5a1Ef492E82`](https://hoodi.etherscan.io/address/0xC372aBC601C4eE5aA82CA2bcb54Da5a1Ef492E82) (impl) ## ๐Ÿ”‘ Execution Delegation Framework {#execution-delegation-framework} - DelegationFactory: [`0xEb49f72DB1546B0E63e1114E2e403edbcE722AE6`](https://hoodi.etherscan.io/address/0xEb49f72DB1546B0E63e1114E2e403edbcE722AE6) ## ๐Ÿ—ณ๏ธ DAO & Aragon Apps {#dao-contracts} - Lido DAO (Kernel): [`0xA48DF029Fd2e5FCECB3886c5c2F60e3625A1E87d`](https://hoodi.etherscan.io/address/0xA48DF029Fd2e5FCECB3886c5c2F60e3625A1E87d) (proxy) - LDO token: [`0xEf2573966D009CcEA0Fc74451dee2193564198dc`](https://hoodi.etherscan.io/address/0xEf2573966D009CcEA0Fc74451dee2193564198dc) - Aragon Voting: [`0x49B3512c44891bef83F8967d075121Bd1b07a01B`](https://hoodi.etherscan.io/address/0x49B3512c44891bef83F8967d075121Bd1b07a01B) (proxy) - Aragon Token Manager: [`0x8ab4a56721Ad8e68c6Ad86F9D9929782A78E39E5`](https://hoodi.etherscan.io/address/0x8ab4a56721Ad8e68c6Ad86F9D9929782A78E39E5) (proxy) - Aragon Finance: [`0x254Ae22bEEba64127F0e59fe8593082F3cd13f6b`](https://hoodi.etherscan.io/address/0x254Ae22bEEba64127F0e59fe8593082F3cd13f6b) (proxy) - Aragon Agent: [`0x0534aA41907c9631fae990960bCC72d75fA7cfeD`](https://hoodi.etherscan.io/address/0x0534aA41907c9631fae990960bCC72d75fA7cfeD) (proxy) - Aragon ACL: [`0x78780e70Eae33e2935814a327f7dB6c01136cc62`](https://hoodi.etherscan.io/address/0x78780e70Eae33e2935814a327f7dB6c01136cc62) (proxy) - Voting Repo: [`0xc972Cdea5956482Ef35BF5852601dD458353cEbD`](https://hoodi.etherscan.io/address/0xc972Cdea5956482Ef35BF5852601dD458353cEbD) (proxy) - Token Manager Repo: [`0xCdE5696e83B1Fb6B8321e35361bB6e9A8bCbfb3f`](https://hoodi.etherscan.io/address/0xCdE5696e83B1Fb6B8321e35361bB6e9A8bCbfb3f) (proxy) - Finance Repo: [`0xa91aA04E0D6a06063d2E878309B60b723D75584d`](https://hoodi.etherscan.io/address/0xa91aA04E0D6a06063d2E878309B60b723D75584d) (proxy) - Agent Repo: [`0x7AA5670B2b4f6f0F7369F4F701C03ebFCe97d130`](https://hoodi.etherscan.io/address/0x7AA5670B2b4f6f0F7369F4F701C03ebFCe97d130) (proxy) - Lido App Repo: [`0xd3545AC0286A94970BacC41D3AF676b89606204F`](https://hoodi.etherscan.io/address/0xd3545AC0286A94970BacC41D3AF676b89606204F) (proxy) - Node Operators Registry Repo: [`0x52eff83071275341ef0A5A2cE48ee818Cef44c39`](https://hoodi.etherscan.io/address/0x52eff83071275341ef0A5A2cE48ee818Cef44c39) (proxy) - Simple DVT Repo: [`0x2b8B52A5e3485853aDccED669B1d0bbF31D40222`](https://hoodi.etherscan.io/address/0x2b8B52A5e3485853aDccED669B1d0bbF31D40222) (proxy) - Sandbox Repo: [`0x89D37eC788988e98BEceB32a8774394F1338B09C`](https://hoodi.etherscan.io/address/0x89D37eC788988e98BEceB32a8774394F1338B09C) (proxy) - EVMScriptRegistry: [`0xe4D32427b1F9b12ab89B142eD3714dCAABB3f38c`](https://hoodi.etherscan.io/address/0xe4D32427b1F9b12ab89B142eD3714dCAABB3f38c) (proxy) - CallsScript: [`0xfB3cB48d81eC8c7f2013a8dc9fA46D2D48112c3A`](https://hoodi.etherscan.io/address/0xfB3cB48d81eC8c7f2013a8dc9fA46D2D48112c3A) - Lido APMRegistry: [`0x15EBf349e1ee9Cd949049fD9352D0c94De046d7b`](https://hoodi.etherscan.io/address/0x15EBf349e1ee9Cd949049fD9352D0c94De046d7b) (proxy) - Aragon APMRegistry: [`0x948ffB5fDA2961C60ED3Eb84c7a31aae42EbEdCC`](https://hoodi.etherscan.io/address/0x948ffB5fDA2961C60ED3Eb84c7a31aae42EbEdCC) (proxy) ### ๐Ÿงฌ Dual Governance {#dual-governance} - Emergency Protected Timelock: [`0x0A5E22782C0Bd4AddF10D771f0bF0406B038282d`](https://hoodi.etherscan.io/address/0x0A5E22782C0Bd4AddF10D771f0bF0406B038282d) - Emergency activation committee: [`0xA678c29cbFde2C74aF15C7724EE4b1527A50D45B`](https://hoodi.etherscan.io/address/0xA678c29cbFde2C74aF15C7724EE4b1527A50D45B) - Emergency execution committee: [`0x8E1Ce8995E370222CbD825fFD7Dce2A5BfE1E631`](https://hoodi.etherscan.io/address/0x8E1Ce8995E370222CbD825fFD7Dce2A5BfE1E631) - Admin Executor: [`0x0eCc17597D292271836691358B22340b78F3035B`](https://hoodi.etherscan.io/address/0x0eCc17597D292271836691358B22340b78F3035B) - Dual Governance: [`0x9CAaCCc62c66d817CC59c44780D1b722359795bF`](https://hoodi.etherscan.io/address/0x9CAaCCc62c66d817CC59c44780D1b722359795bF) - Dual Governance Config Provider: [`0x2b685e6fB288bBb7A82533BAfb679FfDF6E5bb33`](https://hoodi.etherscan.io/address/0x2b685e6fB288bBb7A82533BAfb679FfDF6E5bb33) - Emergency Governance: [`0x69E8e916c4A19F42C13C802abDF2767E1fB4F059`](https://hoodi.etherscan.io/address/0x69E8e916c4A19F42C13C802abDF2767E1fB4F059) - Escrow: [`0x781afe6C8D768CEaA9a97f2A75714e80AE0e83B9`](https://hoodi.etherscan.io/address/0x781afe6C8D768CEaA9a97f2A75714e80AE0e83B9) (proxy) - Escrow: [`0x61b7C2351F63b7f9840736D020eE65D2803A00fb`](https://hoodi.etherscan.io/address/0x61b7C2351F63b7f9840736D020eE65D2803A00fb) (impl) - Reseal Manager: [`0x05172CbCDb7307228F781436b327679e4DAE166B`](https://hoodi.etherscan.io/address/0x05172CbCDb7307228F781436b327679e4DAE166B) - Reseal committee: [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) - Tiebreaker Core Committee: [`0x9Ce4bA766C87cC87e507307163eA54C5003A3563`](https://hoodi.etherscan.io/address/0x9Ce4bA766C87cC87e507307163eA54C5003A3563) - Tiebreaker Sub Committees: - Developers Sub Committee 1: [`0xEd27F0d08630685A0cEFb1040596Cb264cf79f14`](https://hoodi.etherscan.io/address/0xEd27F0d08630685A0cEFb1040596Cb264cf79f14) - Developers Sub Committee 2: [`0xE3e3c67997A4Db7d47ac7fa8ef81B677daBe5794`](https://hoodi.etherscan.io/address/0xE3e3c67997A4Db7d47ac7fa8ef81B677daBe5794) - Developers Sub Committee 3: [`0xF4F16CB3B9E7a076E55c508035f25E606913Cc9d`](https://hoodi.etherscan.io/address/0xF4F16CB3B9E7a076E55c508035f25E606913Cc9d) ## ๐Ÿ”Œ CircuitBreaker {#circuit-breaker} - CircuitBreaker: [`0x44a5789dFeDa59cD176Ab5709ec2F4829dE4d555`](https://hoodi.etherscan.io/address/0x44a5789dFeDa59cD176Ab5709ec2F4829dE4d555) ### Covered pausables and their pausers Each pausable contract below is covered by the CircuitBreaker and has a designated pauser authorized to trigger a pause. | Pausable | Pauser | | --- | --- | | [Withdrawal Queue ERC721](https://hoodi.etherscan.io/address/0xfe56573178f1bcdf53F01A6E9977670dcBBD9186) | [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) | | [Validators Exit Bus Oracle](https://hoodi.etherscan.io/address/0x8664d394C2B3278F26A1B44B967aEf99707eeAB2) | [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) | | [Triggerable Withdrawals Gateway](https://hoodi.etherscan.io/address/0x6679090D92b08a2a686eF8614feECD8cDFE209db) | [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) | | [Vault Hub](https://hoodi.etherscan.io/address/0x4C9fFC325392090F789255b9948Ab1659b797964) | [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) | | [Predeposit Guarantee](https://hoodi.etherscan.io/address/0xa5F55f3402beA2B14AE15Dae1b6811457D43581d) | [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) | | [Consolidation Gateway](https://hoodi.etherscan.io/address/0xC9991Bb865d025364BCbBd99f36e85889Fb68e69) | [`0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102`](https://hoodi.etherscan.io/address/0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102) | | [CSM Module](https://hoodi.etherscan.io/address/0x79CEf36D84743222f37765204Bec41E92a93E59d) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [CSM Accounting](https://hoodi.etherscan.io/address/0xA54b90BA34C5f326BC1485054080994e38FB4C60) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [CSM FeeOracle](https://hoodi.etherscan.io/address/0xe7314f561B2e72f9543F1004e741bab6Fc51028B) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [CSM Verifier](https://hoodi.etherscan.io/address/0xC96406b0eADdAC5708aFCa04DcCA67BAdC9642Fd) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [CSM Ejector](https://hoodi.etherscan.io/address/0xCAe028378d69D54dc8bF809e6C44CF751F997b80) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [VettedGate (IdentifiedCommunityStakersGate)](https://hoodi.etherscan.io/address/0x10a254E724fe2b7f305F76f3F116a3969c53845f) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [VettedGate (IdentifiedDVTClusterGate)](https://hoodi.etherscan.io/address/0x887F8512F9998045f4b5993e6eaa6BCfE5F02A94) | [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53) | | [CM v2 Module](https://hoodi.etherscan.io/address/0x87EB69Ae51317405FD285efD2326a4a11f6173b9) | [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://hoodi.etherscan.io/address/0x84DffcfB232594975C608DE92544Ff239a24c9E9) | | [CM v2 Accounting](https://hoodi.etherscan.io/address/0x7f7356D29aCd915F1934220956c3305808ceB235) | [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://hoodi.etherscan.io/address/0x84DffcfB232594975C608DE92544Ff239a24c9E9) | | [CM v2 FeeOracle](https://hoodi.etherscan.io/address/0x5D2F27000C80f6f7A03015Fd49dB7FEba3fBfa83) | [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://hoodi.etherscan.io/address/0x84DffcfB232594975C608DE92544Ff239a24c9E9) | | [CM v2 Verifier](https://hoodi.etherscan.io/address/0x209190Ebc2Be80367a15d05e626784Eb94d6A880) | [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://hoodi.etherscan.io/address/0x84DffcfB232594975C608DE92544Ff239a24c9E9) | | [CM v2 Ejector](https://hoodi.etherscan.io/address/0xfDbde2B3554B69C84e0f8d7daB68D390Ff0f4394) | [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://hoodi.etherscan.io/address/0x84DffcfB232594975C608DE92544Ff239a24c9E9) | ## ๐Ÿ“Š Data Bus {#data-bus} - DataBus on Chiado (Testnet): [`0x37De961D6bb5865867aDd416be07189D2Dd960e6`](https://gnosis-chiado.blockscout.com/address/0x37De961D6bb5865867aDd416be07189D2Dd960e6) ## ๐Ÿค– Bots {#bots} - Depositor bot: [`0x9b186cE78Ddd6fF098b4a533Dd17a139e1FFeD76`](https://hoodi.etherscan.io/address/0x9b186cE78Ddd6fF098b4a533Dd17a139e1FFeD76) ## ๐Ÿ”„ Post Token Rebase Receiver {#post-token-rebase-receiver} - Token Rate Notifier: [`0xe2d1307a8e0eb6996eE9eB6FB5949124F17EDf65`](https://hoodi.etherscan.io/address/0xe2d1307a8e0eb6996eE9eB6FB5949124F17EDf65) ## ๐Ÿงฉ Staking Modules {#staking-modules} ### ๐Ÿ›ก๏ธ Curated Module - Node Operators Registry: [`0x5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5`](https://hoodi.etherscan.io/address/0x5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5) (proxy) - Node Operators Registry: [`0x95F00b016bB31b7182D96D25074684518246E42a`](https://hoodi.etherscan.io/address/0x95F00b016bB31b7182D96D25074684518246E42a) (impl) ### ๐Ÿงฉ Simple DVT Module - Node Operators Registry: [`0x0B5236BECA68004DB89434462DfC3BB074d2c830`](https://hoodi.etherscan.io/address/0x0B5236BECA68004DB89434462DfC3BB074d2c830) (proxy) - Node Operators Registry: [`0x95F00b016bB31b7182D96D25074684518246E42a`](https://hoodi.etherscan.io/address/0x95F00b016bB31b7182D96D25074684518246E42a) (impl) ### ๐Ÿงช Sandbox Module - Node Operators Registry: [`0x682E94d2630846a503BDeE8b6810DF71C9806891`](https://hoodi.etherscan.io/address/0x682E94d2630846a503BDeE8b6810DF71C9806891) (proxy) - Node Operators Registry: [`0x95F00b016bB31b7182D96D25074684518246E42a`](https://hoodi.etherscan.io/address/0x95F00b016bB31b7182D96D25074684518246E42a) (impl) ### ๐Ÿค Community Staking Module - Entry Gates: - PermissionlessGate: [`0xd7bD8D2A9888D1414c770B35ACF55890B15de26a`](https://hoodi.etherscan.io/address/0xd7bD8D2A9888D1414c770B35ACF55890B15de26a) - VettedGate (IdentifiedCommunityStakersGate): [`0x10a254E724fe2b7f305F76f3F116a3969c53845f`](https://hoodi.etherscan.io/address/0x10a254E724fe2b7f305F76f3F116a3969c53845f) (proxy) - VettedGate (IdentifiedDVTClusterGate): [`0x887F8512F9998045f4b5993e6eaa6BCfE5F02A94`](https://hoodi.etherscan.io/address/0x887F8512F9998045f4b5993e6eaa6BCfE5F02A94) (proxy) - VettedGate: [`0x5Dd9dDC953f2a4352D9C8C42B8D5E2bf535e602F`](https://hoodi.etherscan.io/address/0x5Dd9dDC953f2a4352D9C8C42B8D5E2bf535e602F) (impl, shared) - CSModule: [`0x79CEf36D84743222f37765204Bec41E92a93E59d`](https://hoodi.etherscan.io/address/0x79CEf36D84743222f37765204Bec41E92a93E59d) (proxy) - CSModule: [`0xB48d144A1c7aB43FDb0ac7582C65728E94e1Df0c`](https://hoodi.etherscan.io/address/0xB48d144A1c7aB43FDb0ac7582C65728E94e1Df0c) (impl) - Accounting: [`0xA54b90BA34C5f326BC1485054080994e38FB4C60`](https://hoodi.etherscan.io/address/0xA54b90BA34C5f326BC1485054080994e38FB4C60) (proxy) - Accounting: [`0x690c68287c6e759059d48519274B6920356647F0`](https://hoodi.etherscan.io/address/0x690c68287c6e759059d48519274B6920356647F0) (impl) - ParametersRegistry: [`0xA4aD5236963f9Fe4229864712269D8d79B65C5Ad`](https://hoodi.etherscan.io/address/0xA4aD5236963f9Fe4229864712269D8d79B65C5Ad) (proxy) - ParametersRegistry: [`0x58376D8B192813E85532b25685D948EB49c2A8B5`](https://hoodi.etherscan.io/address/0x58376D8B192813E85532b25685D948EB49c2A8B5) (impl) - FeeDistributor: [`0xaCd9820b0A2229a82dc1A0770307ce5522FF3582`](https://hoodi.etherscan.io/address/0xaCd9820b0A2229a82dc1A0770307ce5522FF3582) (proxy) - FeeDistributor: [`0x74c5be19CcD1a264899FbCf8dB1a64C1e3fb73Ac`](https://hoodi.etherscan.io/address/0x74c5be19CcD1a264899FbCf8dB1a64C1e3fb73Ac) (impl) - Verifier: [`0xC96406b0eADdAC5708aFCa04DcCA67BAdC9642Fd`](https://hoodi.etherscan.io/address/0xC96406b0eADdAC5708aFCa04DcCA67BAdC9642Fd) - FeeOracle: - FeeOracle: [`0xe7314f561B2e72f9543F1004e741bab6Fc51028B`](https://hoodi.etherscan.io/address/0xe7314f561B2e72f9543F1004e741bab6Fc51028B) (proxy) - FeeOracle: [`0x27d1Ff0353AF6b7480CBc902169d0F89b49334B5`](https://hoodi.etherscan.io/address/0x27d1Ff0353AF6b7480CBc902169d0F89b49334B5) (impl) - HashConsensus: [`0x54f74a10e4397dDeF85C4854d9dfcA129D72C637`](https://hoodi.etherscan.io/address/0x54f74a10e4397dDeF85C4854d9dfcA129D72C637) - ValidatorStrikes: [`0x8fBA385C3c334D251eE413e79d4D3890db98693c`](https://hoodi.etherscan.io/address/0x8fBA385C3c334D251eE413e79d4D3890db98693c) (proxy) - ValidatorStrikes: [`0x55D2206a4f7e81c170D3f5b7aFbD46B9f75Bc54d`](https://hoodi.etherscan.io/address/0x55D2206a4f7e81c170D3f5b7aFbD46B9f75Bc54d) (impl) - Ejector: [`0xCAe028378d69D54dc8bF809e6C44CF751F997b80`](https://hoodi.etherscan.io/address/0xCAe028378d69D54dc8bF809e6C44CF751F997b80) - ExitPenalties: [`0xD259b31083Be841E5C85b2D481Cfc17C14276800`](https://hoodi.etherscan.io/address/0xD259b31083Be841E5C85b2D481Cfc17C14276800) (proxy) - ExitPenalties: [`0xf38A3DA25B417D83182EEDD30d00557d78c35C96`](https://hoodi.etherscan.io/address/0xf38A3DA25B417D83182EEDD30d00557d78c35C96) (impl) - Factories: - VettedGateFactory: [`0x8B11fdbA811aeE889F8A55FF58CC0Bf0BbA800e1`](https://hoodi.etherscan.io/address/0x8B11fdbA811aeE889F8A55FF58CC0Bf0BbA800e1) - External libraries: - AssetRecovererLib: [`0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902`](https://hoodi.etherscan.io/address/0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902) - BondCurvesLib: [`0xC4511d09639e5E174506083443da230D39196323`](https://hoodi.etherscan.io/address/0xC4511d09639e5E174506083443da230D39196323) - DepositQueueOps: [`0xb430AA6C70A352c2aaC9813AE049A210dB11aB41`](https://hoodi.etherscan.io/address/0xb430AA6C70A352c2aaC9813AE049A210dB11aB41) - GeneralPenaltyLib: [`0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4`](https://hoodi.etherscan.io/address/0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4) - NOAddresses: [`0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3`](https://hoodi.etherscan.io/address/0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3) - NodeOperatorOps: [`0xDD42EE5D54A1822021782F3F455bb99fBC19499A`](https://hoodi.etherscan.io/address/0xDD42EE5D54A1822021782F3F455bb99fBC19499A) - StakeTracker: [`0xbb6E4Db18182d45038F91B9F1195291c206fd8d2`](https://hoodi.etherscan.io/address/0xbb6E4Db18182d45038F91B9F1195291c206fd8d2) - TopUpQueueOps: [`0xdA104f5f2a18405fC7cCD6E0A7FEB5B824843606`](https://hoodi.etherscan.io/address/0xdA104f5f2a18405fC7cCD6E0A7FEB5B824843606) - WithdrawnValidatorLib: [`0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A`](https://hoodi.etherscan.io/address/0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A) ### ๐Ÿค Community Staking Module 0x02 - Entry Gates: - PermissionlessGate: [`0x5AD784cD0A3291e083b015a81E53c6ec70bd5Ef7`](https://hoodi.etherscan.io/address/0x5AD784cD0A3291e083b015a81E53c6ec70bd5Ef7) - CSModule: [`0xbb7dd81FAC80f3Effa10eA8b973c15AE65a4CAf9`](https://hoodi.etherscan.io/address/0xbb7dd81FAC80f3Effa10eA8b973c15AE65a4CAf9) (proxy) - CSModule: [`0x96C64c0e33D8a52BFedcb1171B8670505020f7A8`](https://hoodi.etherscan.io/address/0x96C64c0e33D8a52BFedcb1171B8670505020f7A8) (impl) - Accounting: [`0x04A0294bF3306532309D7DD776D4A7eF502313e0`](https://hoodi.etherscan.io/address/0x04A0294bF3306532309D7DD776D4A7eF502313e0) (proxy) - Accounting: [`0x3947824a7a893DB70E8bBCA864B2dCE1D74aa8BD`](https://hoodi.etherscan.io/address/0x3947824a7a893DB70E8bBCA864B2dCE1D74aa8BD) (impl) - ParametersRegistry: [`0x81c92Ca47255F1Ab31206b423Af33Ee47c0aE416`](https://hoodi.etherscan.io/address/0x81c92Ca47255F1Ab31206b423Af33Ee47c0aE416) (proxy) - ParametersRegistry: [`0x1d7De9b052d40C6aF59a5743f5A4740C3137242d`](https://hoodi.etherscan.io/address/0x1d7De9b052d40C6aF59a5743f5A4740C3137242d) (impl) - FeeDistributor: [`0x7E875b0cb3725Ff58AF903679d1bF807A3089496`](https://hoodi.etherscan.io/address/0x7E875b0cb3725Ff58AF903679d1bF807A3089496) (proxy) - FeeDistributor: [`0x7Dc7b4215E7b45ca2590f0023a578FAe23914dC2`](https://hoodi.etherscan.io/address/0x7Dc7b4215E7b45ca2590f0023a578FAe23914dC2) (impl) - Verifier: [`0xFdE0FD9aDa4E898D3b34Dd4EA3433b75f0B6dd30`](https://hoodi.etherscan.io/address/0xFdE0FD9aDa4E898D3b34Dd4EA3433b75f0B6dd30) - FeeOracle: - FeeOracle: [`0x9B8bBA11bbE1a351CC8dD1CFCa6719FF7274A208`](https://hoodi.etherscan.io/address/0x9B8bBA11bbE1a351CC8dD1CFCa6719FF7274A208) (proxy) - FeeOracle: [`0xAcA75A0fD7Ab9c9B50ECadb9DDd3321Dc46CEf98`](https://hoodi.etherscan.io/address/0xAcA75A0fD7Ab9c9B50ECadb9DDd3321Dc46CEf98) (impl) - HashConsensus: [`0x41142D077860906B0A7Debb270f1B8e7d1c8BF34`](https://hoodi.etherscan.io/address/0x41142D077860906B0A7Debb270f1B8e7d1c8BF34) - ValidatorStrikes: [`0x543Fbc220A1dAb7f41C62a793D7157Ab6Bd44AA6`](https://hoodi.etherscan.io/address/0x543Fbc220A1dAb7f41C62a793D7157Ab6Bd44AA6) (proxy) - ValidatorStrikes: [`0xE6b521F522103fd7499D7C116834E38F342c9189`](https://hoodi.etherscan.io/address/0xE6b521F522103fd7499D7C116834E38F342c9189) (impl) - Ejector: [`0xf8a71C08DBe7D2efaD76D3951a3065B8cE20e4f0`](https://hoodi.etherscan.io/address/0xf8a71C08DBe7D2efaD76D3951a3065B8cE20e4f0) - ExitPenalties: [`0x3A2a355a27478f4f043e4206b7e1301611642801`](https://hoodi.etherscan.io/address/0x3A2a355a27478f4f043e4206b7e1301611642801) (proxy) - ExitPenalties: [`0xa318a6bEBca9CD02aB3a298AA5ea8eB54746ae81`](https://hoodi.etherscan.io/address/0xa318a6bEBca9CD02aB3a298AA5ea8eB54746ae81) (impl) ### ๐Ÿ›ก๏ธ Curated Module v2 - Entry Gates: - Professional Operator Gate: [`0xF1862d120831eBE31f7202378Ff3Ae63A5658ae3`](https://hoodi.etherscan.io/address/0xF1862d120831eBE31f7202378Ff3Ae63A5658ae3) (proxy) - Professional Trusted Operator Gate: [`0x410A309dF81B782190188CDB3d215729cc6bC1f3`](https://hoodi.etherscan.io/address/0x410A309dF81B782190188CDB3d215729cc6bC1f3) (proxy) - Public Good Operator Gate: [`0xa5A604b172787e017b1b118F02fE54fC1D696519`](https://hoodi.etherscan.io/address/0xa5A604b172787e017b1b118F02fE54fC1D696519) (proxy) - Decentralization Operator Gate: [`0xE966874cDB6A4282ED75Cd10439e3799e5531a2D`](https://hoodi.etherscan.io/address/0xE966874cDB6A4282ED75Cd10439e3799e5531a2D) (proxy) - Extra Effort Operator Gate: [`0x5c063da03e3f21443716D75a2205EE16706e1153`](https://hoodi.etherscan.io/address/0x5c063da03e3f21443716D75a2205EE16706e1153) (proxy) - Intra-Operator DVT Cluster Gate: [`0x1cD655Ac53CfE8269DE0DBfc0140B074623C4A6B`](https://hoodi.etherscan.io/address/0x1cD655Ac53CfE8269DE0DBfc0140B074623C4A6B) (proxy) - Intra-Operator DVT Cluster Plus Gate: [`0x28518be9894C20135F280a9539617783b08a04c7`](https://hoodi.etherscan.io/address/0x28518be9894C20135F280a9539617783b08a04c7) (proxy) - Gate: [`0xA8347dD3fe2f0c8d100B7e224E2B243dF99bA941`](https://hoodi.etherscan.io/address/0xA8347dD3fe2f0c8d100B7e224E2B243dF99bA941) (impl, shared) - CuratedModule: [`0x87EB69Ae51317405FD285efD2326a4a11f6173b9`](https://hoodi.etherscan.io/address/0x87EB69Ae51317405FD285efD2326a4a11f6173b9) (proxy) - CuratedModule: [`0xf01b6b3E27fD3A4c063E391A0714c770d9422CE8`](https://hoodi.etherscan.io/address/0xf01b6b3E27fD3A4c063E391A0714c770d9422CE8) (impl) - Accounting: [`0x7f7356D29aCd915F1934220956c3305808ceB235`](https://hoodi.etherscan.io/address/0x7f7356D29aCd915F1934220956c3305808ceB235) (proxy) - Accounting: [`0x665eA8EFf296f67C59eda4F4130ba12De49c1cc3`](https://hoodi.etherscan.io/address/0x665eA8EFf296f67C59eda4F4130ba12De49c1cc3) (impl) - ParametersRegistry: [`0xefb8e4091A75C4828826bf64595F392f87A07b37`](https://hoodi.etherscan.io/address/0xefb8e4091A75C4828826bf64595F392f87A07b37) (proxy) - ParametersRegistry: [`0x4F5C45d88Fa9fFd409b5a6D933BC41256a893cfb`](https://hoodi.etherscan.io/address/0x4F5C45d88Fa9fFd409b5a6D933BC41256a893cfb) (impl) - MetaRegistry: [`0x857289cCBFBc4C134Cc312022a104CD9b38d8AAE`](https://hoodi.etherscan.io/address/0x857289cCBFBc4C134Cc312022a104CD9b38d8AAE) (proxy) - MetaRegistry: [`0x55C22866805080b12A9876e9892180f1F66a5d6B`](https://hoodi.etherscan.io/address/0x55C22866805080b12A9876e9892180f1F66a5d6B) (impl) - FeeDistributor: [`0x0ced6de191E2A15f7BBAf9E32307626C9f6BD0Cd`](https://hoodi.etherscan.io/address/0x0ced6de191E2A15f7BBAf9E32307626C9f6BD0Cd) (proxy) - FeeDistributor: [`0x505113E2842726FF721634970EFE3f46dD239019`](https://hoodi.etherscan.io/address/0x505113E2842726FF721634970EFE3f46dD239019) (impl) - Verifier: [`0x209190Ebc2Be80367a15d05e626784Eb94d6A880`](https://hoodi.etherscan.io/address/0x209190Ebc2Be80367a15d05e626784Eb94d6A880) - FeeOracle: - FeeOracle: [`0x5D2F27000C80f6f7A03015Fd49dB7FEba3fBfa83`](https://hoodi.etherscan.io/address/0x5D2F27000C80f6f7A03015Fd49dB7FEba3fBfa83) (proxy) - FeeOracle: [`0x5AE7D76050f57D3c42931B3c845ec09b42c3370d`](https://hoodi.etherscan.io/address/0x5AE7D76050f57D3c42931B3c845ec09b42c3370d) (impl) - HashConsensus: [`0x920883908A78c1554f682006a8aB32E62Be09F33`](https://hoodi.etherscan.io/address/0x920883908A78c1554f682006a8aB32E62Be09F33) - ValidatorStrikes: [`0x4c427Ec826F403339719C0FABfb3209e80939eA6`](https://hoodi.etherscan.io/address/0x4c427Ec826F403339719C0FABfb3209e80939eA6) (proxy) - ValidatorStrikes: [`0xc6215d060F26e1F6F34d53d09C626e46bf4a46A3`](https://hoodi.etherscan.io/address/0xc6215d060F26e1F6F34d53d09C626e46bf4a46A3) (impl) - Ejector: [`0xfDbde2B3554B69C84e0f8d7daB68D390Ff0f4394`](https://hoodi.etherscan.io/address/0xfDbde2B3554B69C84e0f8d7daB68D390Ff0f4394) - ExitPenalties: [`0xad79e1d3B380cEb1a0e188fBAB91f85A446E9E54`](https://hoodi.etherscan.io/address/0xad79e1d3B380cEb1a0e188fBAB91f85A446E9E54) (proxy) - ExitPenalties: [`0xBed0DC3db54ff9cc0B5C1B17292d85681783b029`](https://hoodi.etherscan.io/address/0xBed0DC3db54ff9cc0B5C1B17292d85681783b029) (impl) - Factories: - GateFactory: [`0x0e26d2cC3f0c2A17D2D784068cAAD34206B6804D`](https://hoodi.etherscan.io/address/0x0e26d2cC3f0c2A17D2D784068cAAD34206B6804D) - External libraries: - AssetRecovererLib: [`0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902`](https://hoodi.etherscan.io/address/0x37aDa408AE3c3992953688e2CCb9eE7a3dfdA902) - BondCurvesLib: [`0xC4511d09639e5E174506083443da230D39196323`](https://hoodi.etherscan.io/address/0xC4511d09639e5E174506083443da230D39196323) - CuratedDepositAllocator: [`0xa4fCD4dDa0e4a847142E3592C97c77d8B9B3Cf5F`](https://hoodi.etherscan.io/address/0xa4fCD4dDa0e4a847142E3592C97c77d8B9B3Cf5F) - GeneralPenaltyLib: [`0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4`](https://hoodi.etherscan.io/address/0xF05545ED71c60bBba6E73B6B70B15D4f5F22C0f4) - NOAddresses: [`0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3`](https://hoodi.etherscan.io/address/0x9D9c8799189c797f6e2dA74F71aDF84492adA7D3) - NodeOperatorOps: [`0xDD42EE5D54A1822021782F3F455bb99fBC19499A`](https://hoodi.etherscan.io/address/0xDD42EE5D54A1822021782F3F455bb99fBC19499A) - StakeTracker: [`0xbb6E4Db18182d45038F91B9F1195291c206fd8d2`](https://hoodi.etherscan.io/address/0xbb6E4Db18182d45038F91B9F1195291c206fd8d2) - WithdrawnValidatorLib: [`0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A`](https://hoodi.etherscan.io/address/0x3bf9674f062aF9BA94FdAe9Fcdf2D0001FFf0a3A) ## โšก Easy Track {#easy-track} - EasyTrack: [`0x284D91a7D47850d21A6DEaaC6E538AC7E5E6fc2a`](https://hoodi.etherscan.io/address/0x284D91a7D47850d21A6DEaaC6E538AC7E5E6fc2a) - EVMScriptExecutor: [`0x79a20FD0FA36453B2F45eAbab19bfef43575Ba9E`](https://hoodi.etherscan.io/address/0x79a20FD0FA36453B2F45eAbab19bfef43575Ba9E) ### โš™๏ธ Easy Track Factories for Core Protocol {#easy-track-factories-for-core-protocol} - SetDepositsReserveTarget: [`0x68009122a394504E8fD7fee58F92Cd73c6A60717`](https://hoodi.etherscan.io/address/0x68009122a394504E8fD7fee58F92Cd73c6A60717) ### ๐Ÿงฉ Easy Track factories for staking modules {#easy-track-factories-for-staking-modules} - **Curated Node Operators staking module** (registry: [`0x5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5`](https://hoodi.etherscan.io/address/0x5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5)) - IncreaseNodeOperatorStakingLimit: [`0x0f121e4069e17a2Dc5bAbF39d769313a1e20f323`](https://hoodi.etherscan.io/address/0x0f121e4069e17a2Dc5bAbF39d769313a1e20f323) - CuratedSubmitExitRequestHashes: [`0x397206ecdbdcb1A55A75e60Fc4D054feC72E5f63`](https://hoodi.etherscan.io/address/0x397206ecdbdcb1A55A75e60Fc4D054feC72E5f63) - **Community Staking Module** (module: [`0x79CEf36D84743222f37765204Bec41E92a93E59d`](https://hoodi.etherscan.io/address/0x79CEf36D84743222f37765204Bec41E92a93E59d), trusted caller [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53)) - SetMerkleGateTree: [`0xDAf4afD2dD5DcA705900b9e526150C1a00057994`](https://hoodi.etherscan.io/address/0xDAf4afD2dD5DcA705900b9e526150C1a00057994) - ReportWithdrawalsForSlashedValidators: [`0x5732943077210FD18d9d5d2A9d4D8847A5069713`](https://hoodi.etherscan.io/address/0x5732943077210FD18d9d5d2A9d4D8847A5069713) - SettleGeneralDelayedPenalty: [`0x029239CDF35d5669d81D32A83EbF783b87aD1AEE`](https://hoodi.etherscan.io/address/0x029239CDF35d5669d81D32A83EbF783b87aD1AEE) - UpdateStakingModuleShareLimits: [`0xEE8E0d3087f09f56E3fdb80dd1DB3Fb37de0bfFF`](https://hoodi.etherscan.io/address/0xEE8E0d3087f09f56E3fdb80dd1DB3Fb37de0bfFF) - **Community Staking Module 0x02** (module: [`0xbb7dd81FAC80f3Effa10eA8b973c15AE65a4CAf9`](https://hoodi.etherscan.io/address/0xbb7dd81FAC80f3Effa10eA8b973c15AE65a4CAf9), trusted caller [`0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53`](https://hoodi.etherscan.io/address/0x4AF43Ee34a6fcD1fEcA1e1F832124C763561dA53)) - ReportWithdrawalsForSlashedValidators: [`0x0b384D661101Fe7F56caa421547b243e03ED4E65`](https://hoodi.etherscan.io/address/0x0b384D661101Fe7F56caa421547b243e03ED4E65) - SettleGeneralDelayedPenalty: [`0x2eCf179d5e840e56054E214438008F19E46711bC`](https://hoodi.etherscan.io/address/0x2eCf179d5e840e56054E214438008F19E46711bC) - UpdateStakingModuleShareLimits: [`0x05F2F2eb01A8e8C20FDD07EAb93640cd8304aaC9`](https://hoodi.etherscan.io/address/0x05F2F2eb01A8e8C20FDD07EAb93640cd8304aaC9) - **Curated Module v2** (module: [`0x87EB69Ae51317405FD285efD2326a4a11f6173b9`](https://hoodi.etherscan.io/address/0x87EB69Ae51317405FD285efD2326a4a11f6173b9), trusted caller [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://app.safe.protofire.io/home?safe=hoe:0x84DffcfB232594975C608DE92544Ff239a24c9E9)) - SetMerkleGateTree: [`0x9F4BB90d6D0bB3B18a7156F3648c1e5256BAD1a7`](https://hoodi.etherscan.io/address/0x9F4BB90d6D0bB3B18a7156F3648c1e5256BAD1a7) - ReportWithdrawalsForSlashedValidators: [`0xE1EDc1857B47a3188d9cA16E3e6A2DF2Af494FDD`](https://hoodi.etherscan.io/address/0xE1EDc1857B47a3188d9cA16E3e6A2DF2Af494FDD) - SettleGeneralDelayedPenalty: [`0x6B5b2147E2B7Ae08E4486D41741D805A869d2338`](https://hoodi.etherscan.io/address/0x6B5b2147E2B7Ae08E4486D41741D805A869d2338) - CreateOrUpdateOperatorGroup: [`0xF5Dd3789AC14fd4be9C0D24f4d2218B4024047DD`](https://hoodi.etherscan.io/address/0xF5Dd3789AC14fd4be9C0D24f4d2218B4024047DD) - **Staking Router** (router: [`0xCc820558B39ee15C7C45B59390B503b83fb499A8`](https://hoodi.etherscan.io/address/0xCc820558B39ee15C7C45B59390B503b83fb499A8)) - AllowConsolidationPair: [`0x22D36e7616F541A527989C5652fDA4d527bB461C`](https://hoodi.etherscan.io/address/0x22D36e7616F541A527989C5652fDA4d527bB461C) - **Simple DVT staking module** (registry: [`0x0B5236BECA68004DB89434462DfC3BB074d2c830`](https://hoodi.etherscan.io/address/0x0B5236BECA68004DB89434462DfC3BB074d2c830), trusted caller [`0xbB958292042c604855d23F8db458855d20e16996`](https://app.safe.protofire.io/home?safe=hoe:0xbB958292042c604855d23F8db458855d20e16996)) - AddNodeOperators: [`0x42f2532ab3d41dfD6030db1EC2fF3DBC8DCdf89a`](https://hoodi.etherscan.io/address/0x42f2532ab3d41dfD6030db1EC2fF3DBC8DCdf89a) - ActivateNodeOperators: [`0xfA3B3EE204E1f0f165379326768667300992530e`](https://hoodi.etherscan.io/address/0xfA3B3EE204E1f0f165379326768667300992530e) - DeactivateNodeOperators: [`0x3114bEbC222Faec27DF8AB7f9bD8dF2063d7fc77`](https://hoodi.etherscan.io/address/0x3114bEbC222Faec27DF8AB7f9bD8dF2063d7fc77) - SetVettedValidatorsLimits: [`0x956c5dC6cfc8603b2293bF8399B718cbf61a9dda`](https://hoodi.etherscan.io/address/0x956c5dC6cfc8603b2293bF8399B718cbf61a9dda) - SetNodeOperatorNames: [`0x2F98760650922cf65f1b596635bC5835b6E561d4`](https://hoodi.etherscan.io/address/0x2F98760650922cf65f1b596635bC5835b6E561d4) - SetNodeOperatorRewardAddresses: [`0x3d267e4f8d9dCcc83c2DE66729e6A5B2B0856e31`](https://hoodi.etherscan.io/address/0x3d267e4f8d9dCcc83c2DE66729e6A5B2B0856e31) - UpdateTargetValidatorLimits: [`0xc3975Bc4091B585c57357990155B071111d7f4f8`](https://hoodi.etherscan.io/address/0xc3975Bc4091B585c57357990155B071111d7f4f8) - ChangeNodeOperatorManagers: [`0x8a437cd5685e270cDDb347eeEfEbD22109Fa42a9`](https://hoodi.etherscan.io/address/0x8a437cd5685e270cDDb347eeEfEbD22109Fa42a9) - SDVTSubmitExitRequestHashes: [`0xAa3D6A8B52447F272c1E8FAaA06EA06658bd95E2`](https://hoodi.etherscan.io/address/0xAa3D6A8B52447F272c1E8FAaA06EA06658bd95E2) ### ๐Ÿ’ฐ Easy Track Factories for Token Transfers {#easy-track-factories-for-token-transfers} - **Sandbox stETH** (trusted caller is QA & DAO Ops ms [`0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A`](https://app.safe.protofire.io/home?safe=hoe:0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A)) - AllowedRecipientsRegistry: [`0x7E33f2192c2cEC339493B9193110BC0510d6CBD2`](https://hoodi.etherscan.io/address/0x7E33f2192c2cEC339493B9193110BC0510d6CBD2) - TopUpAllowedRecipients: [`0xE5aE943A3AEFA44AD16438Bc3D2cA7654103F985`](https://hoodi.etherscan.io/address/0xE5aE943A3AEFA44AD16438Bc3D2cA7654103F985) - AddAllowedRecipient: [`0x8f05Cc4cC42745E9723E105D38638683f162e1d9`](https://hoodi.etherscan.io/address/0x8f05Cc4cC42745E9723E105D38638683f162e1d9) - RemoveAllowedRecipient: [`0x86E10ffC7c67A92e0c5E58ae42945213da43D0c7`](https://hoodi.etherscan.io/address/0x86E10ffC7c67A92e0c5E58ae42945213da43D0c7) - **Sandbox stablecoins** (trusted caller is QA & DAO Ops ms [`0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A`](https://app.safe.protofire.io/home?safe=hoe:0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A)) - AllowedRecipientsRegistry: [`0xdf53b1cd4CFE43b6CdA3640Be0e4f1a45126ec61`](https://hoodi.etherscan.io/address/0xdf53b1cd4CFE43b6CdA3640Be0e4f1a45126ec61) - AllowedTokensRegistry: [`0x40Db7E8047C487bD8359289272c717eA3C34D1D3`](https://hoodi.etherscan.io/address/0x40Db7E8047C487bD8359289272c717eA3C34D1D3) - TopUpAllowedRecipients: [`0x9D735eeDfa96F53BF9d31DbE81B51a5d333198dB`](https://hoodi.etherscan.io/address/0x9D735eeDfa96F53BF9d31DbE81B51a5d333198dB) - **Tooling contracts:** - AllowedRecipientsBuilder (single token): [`0xC20129f1dd4DFeD023a6d6A8de9d54A7b61af5CC`](https://hoodi.etherscan.io/address/0xC20129f1dd4DFeD023a6d6A8de9d54A7b61af5CC) - AllowedRecipientsFactory (single token): [`0xFdf256eED0ec8B782065E2aCDb975071033A6110`](https://hoodi.etherscan.io/address/0xFdf256eED0ec8B782065E2aCDb975071033A6110) - AllowedRecipientsBuilder (multi token): [`0xf5436129Cf9d8fa2a1cb6e591347155276550635`](https://hoodi.etherscan.io/address/0xf5436129Cf9d8fa2a1cb6e591347155276550635) - AllowedRecipientsFactory (multi token): [`0x08c48Fef9Cadca882E27d2325D1785858D5c1aE3`](https://hoodi.etherscan.io/address/0x08c48Fef9Cadca882E27d2325D1785858D5c1aE3) - BokkyPooBah's DateTime Library: [`0xd1df0cf660d531fad9eaabd3e7b4e8881e28ae2f`](https://hoodi.etherscan.io/address/0xd1df0cf660d531fad9eaabd3e7b4e8881e28ae2f) - AllowedTokensRegistry: [`0x40Db7E8047C487bD8359289272c717eA3C34D1D3`](https://hoodi.etherscan.io/address/0x40db7e8047c487bd8359289272c717ea3c34d1d3) ### ๐Ÿค– Easy Track Factories for MEV-Boost Relay Allowed List Management {#easy-track-factories-for-mev-boost-relay-allowed-list-management} - **MEV-Boost Relay Allowed List** (trusted caller is QA & DAO Ops ms [`0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A`](https://app.safe.protofire.io/home?safe=hoe:0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A)) - AddMEVBoostRelays: [`0xF02DbeaA1Bbc90226CaB995db4C190DbE25983af`](https://hoodi.etherscan.io/address/0xF02DbeaA1Bbc90226CaB995db4C190DbE25983af) - RemoveMEVBoostRelays: [`0x7FCc2901C6C3D62784cB178B14d44445B038f736`](https://hoodi.etherscan.io/address/0x7FCc2901C6C3D62784cB178B14d44445B038f736) - EditMEVBoostRelay: [`0x27A99a7104190DdA297B222104A6C70A4Ca5A17e`](https://hoodi.etherscan.io/address/0x27A99a7104190DdA297B222104A6C70A4Ca5A17e) ### ๐Ÿ”จ Easy Track Factories for stVaults Management {#easy-track-factories-for-stvaults-management} - **Operator Grid:** (trusted caller is Testnet stVaults Committee ms [`0xeBe5948787Bb3a565F67ccD93cb85A91960c472a`](https://app.safe.protofire.io/home?safe=hoe:0xeBe5948787Bb3a565F67ccD93cb85A91960c472a)) - Register Groups: [`0x50ffc44FF526405dBA3e5a4833B003D93301dDDd`](https://hoodi.etherscan.io/address/0x50ffc44FF526405dBA3e5a4833B003D93301dDDd) - Update Groups Share Limit: [`0x99a645A4137ea171Ce4D43c22d30A71251D6Ed7d`](https://hoodi.etherscan.io/address/0x99a645A4137ea171Ce4D43c22d30A71251D6Ed7d) - Register Tiers: [`0x8182E168f858514328C06b5C21eec975E105D494`](https://hoodi.etherscan.io/address/0x8182E168f858514328C06b5C21eec975E105D494) - Alter Tiers: [`0x9A3Fe18BcD5e7657f6a78Ab895aF125Cacae2c36`](https://hoodi.etherscan.io/address/0x9A3Fe18BcD5e7657f6a78Ab895aF125Cacae2c36) - Set Jail Status: [`0x395E6AF61B6Ba3EC0E72E168A2Ec8204589F357c`](https://hoodi.etherscan.io/address/0x395E6AF61B6Ba3EC0E72E168A2Ec8204589F357c) - Update Vaults Fees: [`0x2D5b8B082d618A8d5DeFE3f4c2b2869e3f1C1a3D`](https://hoodi.etherscan.io/address/0x2D5b8B082d618A8d5DeFE3f4c2b2869e3f1C1a3D) - **Vault Hub:** (trusted caller is Testnet stVaults Committee ms [`0xeBe5948787Bb3a565F67ccD93cb85A91960c472a`](https://app.safe.protofire.io/home?safe=hoe:0xeBe5948787Bb3a565F67ccD93cb85A91960c472a)) - Force Validator Exits: [`0x820e9924C2059d37871acd6eccB578e4a3B15c30`](https://hoodi.etherscan.io/address/0x820e9924C2059d37871acd6eccB578e4a3B15c30) - Socialize Bad Debt: [`0x01C9dB53D7a87c3e47D537c925921fB735bEe6c9`](https://hoodi.etherscan.io/address/0x01C9dB53D7a87c3e47D537c925921fB735bEe6c9) - Set Liability Shares Target: [`0xaccaE3755d63EeaAF2e525E780aEeA8D58700Ab9`](https://hoodi.etherscan.io/address/0xaccaE3755d63EeaAF2e525E780aEeA8D58700Ab9) - VaultsAdapter: [`0x854CF0D7446Faa7AdDFE557cc8aa9FA9b7017910`](https://hoodi.etherscan.io/address/0x854CF0D7446Faa7AdDFE557cc8aa9FA9b7017910) ## ๐Ÿ’ต Testnet Stablecoins {#testnet-stablecoins} - USDC: [`0x97bb030B93faF4684eAC76bA0bf3be5ec7140F36`](https://hoodi.etherscan.io/address/0x97bb030B93faF4684eAC76bA0bf3be5ec7140F36) - USDT: [`0x64f1904d1b419c6889BDf3238e31A138E258eA68`](https://hoodi.etherscan.io/address/0x64f1904d1b419c6889BDf3238e31A138E258eA68) - DAI: [`0x17fc691f6EF57D2CA719d30b8fe040123d4ee319`](https://hoodi.etherscan.io/address/0x17fc691f6EF57D2CA719d30b8fe040123d4ee319) - sUSDS: [`0xDaE6a7669f9aB8b2C4E52464AA6FB7F9402aDc70`](https://hoodi.etherscan.io/address/0xDaE6a7669f9aB8b2C4E52464AA6FB7F9402aDc70) ## ๐Ÿ” Testnet DAO Multisigs/EOAs {#testnet-dao-multisigs} - QA & DAO Ops ms: [`0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A`](https://app.safe.protofire.io/home?safe=hoe:0x418B816A7c3ecA151A31d98e30aa7DAa33aBf83A) - Curated Module Committee (CMC) ms [`0x84DffcfB232594975C608DE92544Ff239a24c9E9`](https://app.safe.protofire.io/home?safe=hoe:0x84DffcfB232594975C608DE92544Ff239a24c9E9) - Simple DVT committee ms [`0xbB958292042c604855d23F8db458855d20e16996`](https://app.safe.protofire.io/home?safe=hoe:0xbB958292042c604855d23F8db458855d20e16996) - Testnet stVaults Committee ms [`0xeBe5948787Bb3a565F67ccD93cb85A91960c472a`](https://app.safe.protofire.io/home?safe=hoe:0xeBe5948787Bb3a565F67ccD93cb85A91960c472a) - CSM admin (Staking Modules team EOA) [`0x4af43ee34a6fcd1feca1e1f832124c763561da53`](https://hoodi.etherscan.io/address/0x4af43ee34a6fcd1feca1e1f832124c763561da53) --- :::caution ๐Ÿงชย Testnet use only stETH obtained on this network **has no real ETH backing** and **cannot** be bridged, swapped or redeemed on mainnet. Please do _not_ send mainnet tokens here โ€“ they will be lost forever. ::: ## ๐Ÿ“š Archived Deployments {#archived-deployments}
Lido V3: Testnet-2 (archive) ### ๐Ÿ›๏ธ Core Protocol (Archive) - Lido Locator: [`0xD7c1B80fA86965B48cCA3aDcCB08E1DAEa291980`](https://hoodi.etherscan.io/address/0xD7c1B80fA86965B48cCA3aDcCB08E1DAEa291980) (proxy) - Lido and stETH token: [`0x2C220A2a91602dd93bEAC7b3A1773cdADE369ba1`](https://hoodi.etherscan.io/address/0x2C220A2a91602dd93bEAC7b3A1773cdADE369ba1) (proxy) - wstETH token: [`0x05F2927c5c2825BC0dCDc14d258a99A36116bE8B`](https://hoodi.etherscan.io/address/0x05F2927c5c2825BC0dCDc14d258a99A36116bE8B) - EIP-712 helper for stETH: [`0xBa4F7888A7Cb803776cc2f64b269a7cC7447cD1f`](https://hoodi.etherscan.io/address/0xBa4F7888A7Cb803776cc2f64b269a7cC7447cD1f) - Staking Router: [`0x7DE7173aeB9CDc06E429910104BD1e61a965f567`](https://hoodi.etherscan.io/address/0x7DE7173aeB9CDc06E429910104BD1e61a965f567) (proxy) - Execution Layer Rewards Vault: [`0x99137683D4AAfaf76C84bD8F6e2Ae6A95DF90912`](https://hoodi.etherscan.io/address/0x99137683D4AAfaf76C84bD8F6e2Ae6A95DF90912) - Withdrawalย Queueย (ERCโ€‘721): [`0x07F941C56f155fA4233f0ed8d351C9Af3152E525`](https://hoodi.etherscan.io/address/0x07F941C56f155fA4233f0ed8d351C9Af3152E525) (proxy) - Withdrawalย Vault: [`0x9659aAa1458E2dba8713018Ffa36c64048345901`](https://hoodi.etherscan.io/address/0x9659aAa1458E2dba8713018Ffa36c64048345901) (proxy) - Burner: [`0xa0f32368d67870f4864A748c910C7Ca9B99e1027`](https://hoodi.etherscan.io/address/0xa0f32368d67870f4864A748c910C7Ca9B99e1027) (proxy) - Min First Allocation Strategy: [`0x4A08C1501a886861C17341317FF7885a5a1e5dB6`](https://hoodi.etherscan.io/address/0x4A08C1501a886861C17341317FF7885a5a1e5dB6) - Accounting: [`0x6adfFb27Dcc6b005988E4f9D408c877643D2d8A6`](https://hoodi.etherscan.io/address/0x6adfFb27Dcc6b005988E4f9D408c877643D2d8A6) (proxy) - Vaultย Hub: [`0x26b92f0fdfeBAf43E5Ea5b5974EeBee95F17Fe08`](https://hoodi.etherscan.io/address/0x26b92f0fdfeBAf43E5Ea5b5974EeBee95F17Fe08) (proxy) - Operatorย Grid: [`0x35dd33A473D492745eD5226Cf940b5b1ef4C111D`](https://hoodi.etherscan.io/address/0x35dd33A473D492745eD5226Cf940b5b1ef4C111D) (proxy) - Predepositย Guarantee: [`0xAcb99d36e19763C210A548019C6F238B67644417`](https://hoodi.etherscan.io/address/0xAcb99d36e19763C210A548019C6F238B67644417) (proxy) - Triggerable Withdrawals Gateway: [`0xb273790D9ddA79E586Da819581f919e29ef6f83C`](https://hoodi.etherscan.io/address/0xb273790D9ddA79E586Da819581f919e29ef6f83C) - Validator Exit Delay Verifier: [`0x1b007bC74aB26Db6413B46A04BAB88104050b142`](https://hoodi.etherscan.io/address/0x1b007bC74aB26Db6413B46A04BAB88104050b142) #### ๐Ÿ”จ stVaults Factory Stack (Archive) - Stakingย Vaultย Factory: [`0x74808E3Fe5B7714b580067Ab02032d19E0cD9f5f`](https://hoodi.etherscan.io/address/0x74808E3Fe5B7714b580067Ab02032d19E0cD9f5f) - Stakingย Vaultย Beacon: [`0x8de3b125221d07b44FCbd2CFD7354251858817B3`](https://hoodi.etherscan.io/address/0x8de3b125221d07b44FCbd2CFD7354251858817B3) - Stakingย Vaultย Implementation: [`0x5ff3782820Fc06cdF5a9ded897a778a6f0840b85`](https://hoodi.etherscan.io/address/0x5ff3782820Fc06cdF5a9ded897a778a6f0840b85) - Dashboard Implementation: [`0xcb3Bb848252F7ca05ED7753Ead0Eb2bdfD2ba878`](https://hoodi.etherscan.io/address/0xcb3Bb848252F7ca05ED7753Ead0Eb2bdfD2ba878) - Validator Consolidation Requests: [`0xD69239eFd4812E70238D9E3a80945C9138a241f6`](https://hoodi.etherscan.io/address/0xD69239eFd4812E70238D9E3a80945C9138a241f6) ### ๐Ÿ”ฎ Oracle Contracts (Archive) - Accounting Oracle: - AccountingOracle: [`0x43b319f67F9c48Ca76AA60d8693dc63E3B94698F`](https://hoodi.etherscan.io/address/0x43b319f67F9c48Ca76AA60d8693dc63E3B94698F) (proxy) - Hash Consensus for Accounting Oracle: [`0x49C3eCB0F8C32a6F00be2848BE3Edb09Ef0646D9`](https://hoodi.etherscan.io/address/0x49C3eCB0F8C32a6F00be2848BE3Edb09Ef0646D9) - Validators Exit Bus Oracle: - ValidatorsExitBusOracle: [`0xF1D059331C81C4ac9ACe81e3cE1a4961d59413f8`](https://hoodi.etherscan.io/address/0xF1D059331C81C4ac9ACe81e3cE1a4961d59413f8) (proxy) - Hash Consensus for Validatorsย Exitย Busย Oracle: [`0xd7890f55266A795b59E9468Cd37a8524FBf44EFd`](https://hoodi.etherscan.io/address/0xd7890f55266A795b59E9468Cd37a8524FBf44EFd) - Oracleย Reportย Sanityย Checker: [`0x90F33A702E0DD5F050bA4910cCd3DC8b60C0901e`](https://hoodi.etherscan.io/address/0x90F33A702E0DD5F050bA4910cCd3DC8b60C0901e) - Oracleย Daemonย Config: [`0x2cB903dA5DB2Ad46E367F32499fB2781E0D2eD7D`](https://hoodi.etherscan.io/address/0x2cB903dA5DB2Ad46E367F32499fB2781E0D2eD7D) - Lazy Oracle: [`0xdF66Fb038CbB7587cC52A397CA88143657f3Ae4A`](https://hoodi.etherscan.io/address/0xdF66Fb038CbB7587cC52A397CA88143657f3Ae4A) ### ๐Ÿ—ณ๏ธ DAO & Aragon Apps (Archive) - Lido DAO (Kernel): [`0x207BAA2a636f094eCCBaA70FDE74D31723b7709c`](https://hoodi.etherscan.io/address/0x207BAA2a636f094eCCBaA70FDE74D31723b7709c) (proxy) - LDO token: [`0xbfd40Db0a3CB72cF936353CE4EA6cdbBeB65F1Db`](https://hoodi.etherscan.io/address/0xbfd40Db0a3CB72cF936353CE4EA6cdbBeB65F1Db) - Aragon Voting: [`0x3DF09262F937a92b9d7CC020e22709b6c6641d7d`](https://hoodi.etherscan.io/address/0x3DF09262F937a92b9d7CC020e22709b6c6641d7d) (proxy) - Aragon Token Manager: [`0xB769867675CD2e3c2ea7b29b5Bd282dC1C00Ad66`](https://hoodi.etherscan.io/address/0xB769867675CD2e3c2ea7b29b5Bd282dC1C00Ad66) (proxy) - Aragon Finance: [`0x86eAE4CBb13e5d7f8f4a3582F24F6133047672F2`](https://hoodi.etherscan.io/address/0x86eAE4CBb13e5d7f8f4a3582F24F6133047672F2) (proxy) - Aragon Agent: [`0xEB9712bf5DD2179EEacc45A62A69b156299084a7`](https://hoodi.etherscan.io/address/0xEB9712bf5DD2179EEacc45A62A69b156299084a7) (proxy) - Aragon ACL: [`0xF55a0c7Da6932eBd859Bd7AE896757959785340e`](https://hoodi.etherscan.io/address/0xF55a0c7Da6932eBd859Bd7AE896757959785340e) (proxy) ### ๐Ÿงฉ Staking Modules (Archive) #### ๐Ÿ›ก๏ธ Curated Module (Archive) - Node Operators Registry: [`0xa38DE5874E81561F29cfa4436111852CC34aC1e1`](https://hoodi.etherscan.io/address/0xa38DE5874E81561F29cfa4436111852CC34aC1e1) (proxy) #### ๐Ÿงฉ Simple DVT Module (Archive) - Simple DVT: [`0x0718D0A48D9B3Fd6E03B10249655539DB4Bf63c4`](https://hoodi.etherscan.io/address/0x0718D0A48D9B3Fd6E03B10249655539DB4Bf63c4) (proxy) ### โšก Easy Track (Archive) - EasyTrack: [`0x2b2b29E8C0f0fA5D16057Ca0cdC9B4152d4B8C9C`](https://hoodi.etherscan.io/address/0x2b2b29E8C0f0fA5D16057Ca0cdC9B4152d4B8C9C) - EVMScriptExecutor: [`0xbf91a57E194c2c7a758247eC12648Fc5651478db`](https://hoodi.etherscan.io/address/0xbf91a57E194c2c7a758247eC12648Fc5651478db) - Factories: - DecreaseShareLimitsInVaultHub: [`0x96B4215538d1B838a6A452d6F50c02e7fA258f43`](https://hoodi.etherscan.io/address/0x96B4215538d1B838a6A452d6F50c02e7fA258f43) - DecreaseVaultsFeesInVaultHub: [`0x806F27100347d735819f25B75Be3f0acBd6aEbAF`](https://hoodi.etherscan.io/address/0x806F27100347d735819f25B75Be3f0acBd6aEbAF) - ForceValidatorExitsInVaultHub: [`0xd0E3b451495E63923e45fC95f5Dc2e16c55e4209`](https://hoodi.etherscan.io/address/0xd0E3b451495E63923e45fC95f5Dc2e16c55e4209) - SocializeBadDebtInVaultHub: [`0x921A2D3efcE8b66b0CA9493a2C26AEf69aFC8f1E`](https://hoodi.etherscan.io/address/0x921A2D3efcE8b66b0CA9493a2C26AEf69aFC8f1E) - SetVaultRedemptionsInVaultHub: [`0x8562BA0D14851cb2cB88d7E1497968e9001E7f94`](https://hoodi.etherscan.io/address/0x8562BA0D14851cb2cB88d7E1497968e9001E7f94) - RegisterGroupsInOperatorGrid: [`0x89a7472DD79dDEb731Bc7B3Aad6ba42666616D22`](https://hoodi.etherscan.io/address/0x89a7472DD79dDEb731Bc7B3Aad6ba42666616D22) - UpdateGroupsShareLimitInOperatorGrid: [`0x34086e861a46F378AA89a53DCA8fF6eB03d4a0Ab`](https://hoodi.etherscan.io/address/0x34086e861a46F378AA89a53DCA8fF6eB03d4a0Ab) - RegisterTiersInOperatorGrid: [`0xB824727CA93C7f2C7749ce4F3FaCB138EbB46854`](https://hoodi.etherscan.io/address/0xB824727CA93C7f2C7749ce4F3FaCB138EbB46854) - AlterTiersInOperatorGrid:[`0xD4aF3d17efd18DF0D6a84b8111b9Cd71A039E4a4`](https://hoodi.etherscan.io/address/0xD4aF3d17efd18DF0D6a84b8111b9Cd71A039E4a4) - CSMSettleElStealingPenalty: [`0x5c0af5b9f96921d3F61503e1006CF0ab9867279E`](https://hoodi.etherscan.io/address/0x5c0af5b9f96921d3F61503e1006CF0ab9867279E) - CSMSetVettedGateTree: [`0xa890fc73e1b771Ee6073e2402E631c312FF92Cd9`](https://hoodi.etherscan.io/address/0xa890fc73e1b771Ee6073e2402E631c312FF92Cd9) - Adapters: - VaultHubAdapter: [`0xb4A1E35cdE96A9E36542bDC3aDb276542a2378b4`](https://hoodi.etherscan.io/address/0xb4A1E35cdE96A9E36542bDC3aDb276542a2378b4) #### โ›๏ธ Special accounts and addresses
Show/hide | Contract | Address | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Deposit Security Module (EOA stub) | [`0xfF772cd178D04F0B4b1EFB730c5F2B9683B31611`](https://hoodi.etherscan.io/address/0xfF772cd178D04F0B4b1EFB730c5F2B9683B31611) | | Deployer (EOA) | [`0x26EDb7f0f223A25EE390aCCccb577F3a31edDfC5`](https://hoodi.etherscan.io/address/0x26EDb7f0f223A25EE390aCCccb577F3a31edDfC5) |
---
--- # Hoodi: Lido V3 testnet-2 (Archived) ::::warning ๐Ÿ”ฌ **Deprecation Notice: Lido V3 Testnet-2 Sunset** **This deployment is deprecated and will be decommissioned soon.** :::: **Action Required:** - โš ๏ธ **Please stop using Lido V3 testnet-2 contracts** - ๐Ÿ”„ **Migrate to the latest Hoodi deployment** โ†’ [Hoodi (testnet-3 for stVaults)](/docs/deployed-contracts/hoodi.md) - ๐Ÿ“‹ **Update your integration configurations** to point to the new contract addresses - ๐Ÿ—‘๏ธ Test tokens on testnet-2 may become unusable after sunset --- **Why migrate?** The latest Lido V3 long-lived testnet (testnet-3) is now the **main operational protocol instance on Hoodi**, featuring: - โœ… Latest protocol upgrades and up-to-date Lido V3 audit fixes included - โœ… Full stVaults support with Predeposit Guarantee (PDG) - โœ… Active oracle operations and maintenance with triggerable withdrawals - โœ… Complete Easy Track governance functionality - โœ… Up-to-date Community Staking Module (CSM 2.0) - โœ… Close to mainnet configuration ๐Ÿ‘‰ **[View main protocol deployment on Hoodi](/docs/deployed-contracts/hoodi.md)** --- # Sepolia :::warning The **Sepolia** deployment is now fully **deprecated**. Please use the [**Hoodi**](/deployed-contracts/hoodi.md) deployment instead. ::: :::info Sepolia testnet has had only a limited set of working parts of the protocol. The goals for this testnet deployment were: - Have end-to-end testnet for Lido Multichain - The running-in of new zk-based oracles (based on [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) availability) There will be no comprehensive Lido testnet environment available for Sepolia due to the network's restricted and permission-based [validator set](https://github.com/eth-clients/sepolia/issues/12) configuration. ::: ## Core protocol - Lido Locator: [`0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7`](https://sepolia.etherscan.io/address/0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7) (proxy) - Lido and stETH token: [`0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af`](https://sepolia.etherscan.io/address/0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af) (proxy) - wstETH token: [`0xB82381A3fBD3FaFA77B3a7bE693342618240067b`](https://sepolia.etherscan.io/address/0xB82381A3fBD3FaFA77B3a7bE693342618240067b) - wstETH referral staker: [`0x9E90338495FfD691bDDC680e47D94b60cF66dDad`](https://sepolia.etherscan.io/address/0x9E90338495FfD691bDDC680e47D94b60cF66dDad) - EIP-712 helper for stETH: [`0x9726CA9AEFF4BC8FB8C084BdAbdB71608248E3f8`](https://sepolia.etherscan.io/address/0x9726CA9AEFF4BC8FB8C084BdAbdB71608248E3f8) - StakingRouter: [`0x4F36aAEb18Ab56A4e380241bea6ebF215b9cb12c`](https://sepolia.etherscan.io/address/0x4F36aAEb18Ab56A4e380241bea6ebF215b9cb12c) (proxy) - Node Operators registry: [`0x33d6E15047E8644F8DDf5CD05d202dfE587DA6E3`](https://sepolia.etherscan.io/address/0x33d6E15047E8644F8DDf5CD05d202dfE587DA6E3) (proxy) - Deposit Security Module (EOA replacement): [`0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5`](https://sepolia.etherscan.io/address/0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5) - Execution Layer Rewards Vault: [`0x94B1B8e2680882f8652882e7F196169dE3d9a3B2`](https://sepolia.etherscan.io/address/0x94B1B8e2680882f8652882e7F196169dE3d9a3B2) - Withdrawal Queue ERC721: [`0x1583C7b3f4C3B008720E6BcE5726336b0aB25fdd`](https://sepolia.etherscan.io/address/0x1583C7b3f4C3B008720E6BcE5726336b0aB25fdd) (proxy) - Withdrawal Vault: [`0xDe7318Afa67eaD6d6bbC8224dfCe5ed6e4b86d76`](https://sepolia.etherscan.io/address/0xDe7318Afa67eaD6d6bbC8224dfCe5ed6e4b86d76) (proxy) - Burner: [`0x61Bb0Ef69262d5EF1cc2873cf61766751D99B699`](https://sepolia.etherscan.io/address/0x61Bb0Ef69262d5EF1cc2873cf61766751D99B699) ## Sepolia deposit contract ad-hoc adapter - SepoliaDepositAdapter: [`0x80b5DC88C98E528bF9cb4B7F0f076aC41da24651`](https://sepolia.etherscan.io/address/0x80b5DC88C98E528bF9cb4B7F0f076aC41da24651) (proxy) - SepoliaDepositAdapter: [`0xCcC9E7F5eF7695a7a36Fe08d2086E51eF6Df948f`](https://sepolia.etherscan.io/address/0xCcC9E7F5eF7695a7a36Fe08d2086E51eF6Df948f) (impl) ## Oracle Contracts - Accounting Oracle: - AccountingOracle: [`0xd497Be005638efCf09F6BFC8DAFBBB0BB72cD991`](https://sepolia.etherscan.io/address/0xd497Be005638efCf09F6BFC8DAFBBB0BB72cD991) (proxy) - HashConsensus: [`0x758D8c3CE794b3Dfe3b3A3482B7eD33de2109D95`](https://sepolia.etherscan.io/address/0x758D8c3CE794b3Dfe3b3A3482B7eD33de2109D95) - Validators Exit Bus Oracle: - ValidatorsExitBusOracle: [`0x7637d44c9f2e9cA584a8B5D2EA493012A5cdaEB6`](https://sepolia.etherscan.io/address/0x7637d44c9f2e9cA584a8B5D2EA493012A5cdaEB6) (proxy) - HashConsensus: [`0x098a952BD200005382aEb3229e38ae39A7616F56`](https://sepolia.etherscan.io/address/0x098a952BD200005382aEb3229e38ae39A7616F56) - OracleReportSanityChecker: [`0x69CBE6A06a450f2a2b8942b1091D53490b5A7A15`](https://sepolia.etherscan.io/address/0x69CBE6A06a450f2a2b8942b1091D53490b5A7A15) - OracleDaemonConfig: [`0x7bC76076b0f3879b4A750450C0Ccf02c6Ca11220`](https://sepolia.etherscan.io/address/0x7bC76076b0f3879b4A750450C0Ccf02c6Ca11220) ## DAO contracts - Lido DAO (Kernel): [`0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916`](https://sepolia.etherscan.io/address/0x6155bD199ECcc79Ff4e8B392f6cBD9c9874E8916) (proxy) - LDO token: [`0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB`](https://sepolia.etherscan.io/address/0xd06dF83b8ad6D89C86a187fba4Eae918d497BdCB) - Aragon Voting: [`0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A`](https://sepolia.etherscan.io/address/0x39A0EbdEE54cB319f4F42141daaBDb6ba25D341A) (proxy) - Aragon Token Manager: [`0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66`](https://sepolia.etherscan.io/address/0xC73cd4B2A7c1CBC5BF046eB4A7019365558ABF66) (proxy) - Aragon Finance: [`0x52AD3004Bc993d63931142Dd4f3DD647414048a1`](https://sepolia.etherscan.io/address/0x52AD3004Bc993d63931142Dd4f3DD647414048a1) (proxy) - Aragon Agent: [`0x32A0E5828B62AAb932362a4816ae03b860b65e83`](https://sepolia.etherscan.io/address/0x32A0E5828B62AAb932362a4816ae03b860b65e83) (proxy) - Aragon ACL: [`0x8A1AA86d35b2EE8C9369618E7D7b40000cCD3295`](https://sepolia.etherscan.io/address/0x8A1AA86d35b2EE8C9369618E7D7b40000cCD3295) (proxy) - Lido APMRegistry: [`0x63C2b6310911a234A6A2c4E7dCd9218Ce79b9A9c`](https://sepolia.etherscan.io/address/0x63C2b6310911a234A6A2c4E7dCd9218Ce79b9A9c) (proxy) - Aragon APMRegistry: [`0xDC7F3C08AA574d39Fc97fDCE20f8fA68EA2A0f22`](https://sepolia.etherscan.io/address/0xDC7F3C08AA574d39Fc97fDCE20f8fA68EA2A0f22) (proxy) ## Testnet ad-hoc addresses - Deployer: [`0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5`](https://sepolia.etherscan.io/address/0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5) - Emergency breaks (EOA replacement): [`0xa5F1d7D49F581136Cf6e58B32cBE9a2039C48bA1`](https://sepolia.etherscan.io/address/0xa5F1d7D49F581136Cf6e58B32cBE9a2039C48bA1) ## Lido Multichain ### Optimism ##### Ethereum part - TokenRateNotifier: [`0x10cA9008D7dcea1Bed4d5394F8c58F3113A2814D`](https://sepolia.etherscan.io/address/0x10cA9008D7dcea1Bed4d5394F8c58F3113A2814D) - OpStackTokenRatePusher: [`0x4067B05a6B2f6801Bfb8d4fF417eD32e71c216d9`](https://sepolia.etherscan.io/address/0x4067B05a6B2f6801Bfb8d4fF417eD32e71c216d9) - L1LidoTokensBridge: [`0x4Abf633d9c0F4aEebB4C2E3213c7aa1b8505D332`](https://sepolia.etherscan.io/address/0x4Abf633d9c0F4aEebB4C2E3213c7aa1b8505D332) (proxy) - L1LidoTokensBridge: [`0x8375029773953d91CaCfa452b7D24556b9F318AA`](https://sepolia.etherscan.io/address/0x8375029773953d91CaCfa452b7D24556b9F318AA) (impl) ##### Optimism part - WstETH ERC20BridgedPermit: [`0x24B47cd3A74f1799b32B2de11073764Cb1bb318B`](https://sepolia-optimism.etherscan.io/address/0x24B47cd3A74f1799b32B2de11073764Cb1bb318B) (proxy) - WstETH ERC20BridgedPermit: [`0x298953B9426eba4F35a137a4754278a16d97A063`](https://sepolia-optimism.etherscan.io/address/0x298953B9426eba4F35a137a4754278a16d97A063) (impl) - StETH ERC20RebasableBridgedPermit: [`0xf49D208B5C7b10415C7BeAFe9e656F2DF9eDfe3B`](https://sepolia-optimism.etherscan.io/address/0xf49D208B5C7b10415C7BeAFe9e656F2DF9eDfe3B) (proxy) - StETH ERC20RebasableBridgedPermit: [`0xFd21C82c99ddFa56EB0B9B2D1d0709b7E26D1B2C`](https://sepolia-optimism.etherscan.io/address/0xFd21C82c99ddFa56EB0B9B2D1d0709b7E26D1B2C) (impl) - TokenRateOracle: [`0xB34F2747BCd9BCC4107A0ccEb43D5dcdd7Fabf89`](https://sepolia-optimism.etherscan.io/address/0xB34F2747BCd9BCC4107A0ccEb43D5dcdd7Fabf89) (proxy) - TokenRateOracle: [`0xa989A4B3A26e28DC9d106F163B2B1f35153E0517`](https://sepolia-optimism.etherscan.io/address/0xa989A4B3A26e28DC9d106F163B2B1f35153E0517) (impl) - L2ERC20ExtendedTokensBridge: [`0xdBA2760246f315203F8B716b3a7590F0FFdc704a`](https://sepolia-optimism.etherscan.io/address/0xdBA2760246f315203F8B716b3a7590F0FFdc704a) (proxy) - L2ERC20ExtendedTokensBridge: [`0xD48c69358193a34aC035ea7dfB70daDea1600112`](https://sepolia-optimism.etherscan.io/address/0xD48c69358193a34aC035ea7dfB70daDea1600112) (impl) - Optimism Governance Bridge Executor: [`0xf695357C66bA514150Da95b189acb37b46DDe602`](https://sepolia-optimism.etherscan.io/address/0xf695357C66bA514150Da95b189acb37b46DDe602) ### Scroll ##### Ethereum part - L1LidoGateway: [`0xF22B24fa7c3168f30b17fd97b71bdd3162DDe029`](https://sepolia.etherscan.io/address/0xF22B24fa7c3168f30b17fd97b71bdd3162DDe029) (proxy) - L1LidoGateway: [`0x99845934FC8Ed44F3E6e66b3BAecf24d9e457F7f`](https://sepolia.etherscan.io/address/0x99845934FC8Ed44F3E6e66b3BAecf24d9e457F7f) (impl) - ProxyAdmin: [`0x0dB416f4387ED89c1C99955fe0Ecad458f07c467`](https://sepolia.etherscan.io/address/0x0dB416f4387ED89c1C99955fe0Ecad458f07c467) for L1LidoGateway ##### Scroll part - ScrollBridgeExecutor: [`0x6b314986E3737Ce23c2a13036e77b3f5A846F8AF`](https://sepolia.scrollscan.com/address/0x6b314986E3737Ce23c2a13036e77b3f5A846F8AF) - L2LidoGateway: [`0x635B054A092F6aE61Ce0Fddc397A704F6626510D`](https://sepolia.scrollscan.com/address/0x635B054A092F6aE61Ce0Fddc397A704F6626510D) (proxy) - L2LidoGateway: [`0x906CD1Bfa5C3f7B2FF9BFBB5950ada841ED99E72`](https://sepolia.scrollscan.com/address/0x906CD1Bfa5C3f7B2FF9BFBB5950ada841ED99E72) (impl) - L2WstETHToken: [`0x2DAf22Caf40404ad8ff0Ab1E77F9C08Fef3953e2`](https://sepolia.scrollscan.com/address/0x2DAf22Caf40404ad8ff0Ab1E77F9C08Fef3953e2) (proxy) - L2WstETHToken: [`0xaed405fc13d66e2f1055f6efe9a5ce736652fa55`](https://sepolia.scrollscan.com/address/0xaed405fc13d66e2f1055f6efe9a5ce736652fa55) (impl) - ProxyAdmin: [`0xc6cdc2839378d50e03c9737723d96d117b09bda5`](https://sepolia.scrollscan.com/address/0xc6cdc2839378d50e03c9737723d96d117b09bda5) for: - L2LidoGateway - L2WstETHToken ### Mode ##### Ethereum part - L1ERC20TokenBridge: [`0x16B929D35B200EA0ae0B93EABc3Bf9Ad611BF18F`](https://sepolia.etherscan.io/address/0x16B929D35B200EA0ae0B93EABc3Bf9Ad611BF18F) (proxy) - L1ERC20TokenBridge: [`0x8c1E68A74E71594925c7D7a78Ef43657aec4d599`](https://sepolia.etherscan.io/address/0x8c1E68A74E71594925c7D7a78Ef43657aec4d599) (impl) ##### Mode part - WstETH ERC20Bridged: [`0x2C937931B5d544E3dff1d9D78c80d9772B55837A`](https://sepolia.explorer.mode.network/address/0x2C937931B5d544E3dff1d9D78c80d9772B55837A) (proxy) - WstETH ERC20Bridged: [`0xC9C796E4CefdD38Eb462E84246bE41d500149162`](https://sepolia.explorer.mode.network/address/0xC9C796E4CefdD38Eb462E84246bE41d500149162) (impl) - L2ERC20TokenBridge: [`0xd41a90e55bcfC1CbF96D78aE80BbCB56A6BA0008`](https://sepolia.explorer.mode.network/address/0xd41a90e55bcfC1CbF96D78aE80BbCB56A6BA0008) (proxy) - L2ERC20TokenBridge: [`0x8Cc0183D53c8fB160BFB01fe49ff3E8A9Aa0B1F6`](https://sepolia.explorer.mode.network/address/0x8Cc0183D53c8fB160BFB01fe49ff3E8A9Aa0B1F6) (impl) - Optimism Governance Bridge Executor: [`0x442a6Bea15718588391C5d1dE261AB2c617eA703`](https://sepolia.explorer.mode.network/address/0x442a6Bea15718588391C5d1dE261AB2c617eA703) ### Binance Smart Chain (BSC) ##### Ethereum part ###### a.DI governance forwarding - CrossChainController: [`0x9d8548963Fa0a9BE7C434cA482dd5b79E8062d3A`](https://sepolia.etherscan.io/address/0x9d8548963Fa0a9BE7C434cA482dd5b79E8062d3A) (proxy) - CrossChainController: [`0x57B3C8DC50d1C881fCB384Da4d66f3d610671177`](https://sepolia.etherscan.io/address/0x57B3C8DC50d1C881fCB384Da4d66f3d610671177) (impl) - ProxyAdmin [`0x7BE89331452883D335C2556d1863CD2925E76afc`](https://sepolia.etherscan.io/address/0x7BE89331452883D335C2556d1863CD2925E76afc) for CrossChainController - CCIPAdapterTestnet: [`0xA0362E6D6f399A3dca79a20cf6041807F7Bfd89e`](https://sepolia.etherscan.io/address/0xA0362E6D6f399A3dca79a20cf6041807F7Bfd89e) - HyperLaneAdapter: [`0x9aa88aD35da12C89F5514d04e3BBd8CD95fDf428`](https://sepolia.etherscan.io/address/0x9aa88aD35da12C89F5514d04e3BBd8CD95fDf428) - LayerZeroAdapterTestnet: [`0xFA3199330C9F33e5bA2D559574033D9cf3FCb609`](https://sepolia.etherscan.io/address/0xFA3199330C9F33e5bA2D559574033D9cf3FCb609) - WormholeAdapterTestnet: [`0x82C16B1e054fa94bf60b54A1Aa9FA74c5872899d`](https://sepolia.etherscan.io/address/0x82C16B1e054fa94bf60b54A1Aa9FA74c5872899d) ###### wstETH on BSC endpoints - NTT Manager: [`0x8B715EAf61A7DdF61C67d5D46687c796D1f47146`](https://sepolia.etherscan.io/address/0x8B715EAf61A7DdF61C67d5D46687c796D1f47146) (proxy) - NTT Manager: [`0x607b139bfee21b2676ee664a237a70d737b9466e`](https://sepolia.etherscan.io/address/0x607b139bfee21b2676ee664a237a70d737b9466e) (impl) - Wormhole Transceiver: [`0xF2bc73502283fcaC4b047dfE45366d8744daaC5B`](https://sepolia.etherscan.io/address/0xF2bc73502283fcaC4b047dfE45366d8744daaC5B) - Axelar Transceiver: [`0xaa8267908e8d2BEfeB601f88A7Cf3ec148039423`](https://sepolia.etherscan.io/address/0xaa8267908e8d2BEfeB601f88A7Cf3ec148039423) - Transceiver Structs: [`0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b`](https://sepolia.etherscan.io/address/0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b) ##### BSC part ###### a.DI governance forwarding - CrossChainController: [`0x1FAa7AFD7851e7Cf931053e49CE26D4E262698b6`](https://testnet.bscscan.com/address/0x1FAa7AFD7851e7Cf931053e49CE26D4E262698b6) (proxy) - CrossChainController: [`0x5EC23B39E6E8eb5BA0c7064a0c08b5e678b02F37`](https://testnet.bscscan.com/address/0x5EC23B39E6E8eb5BA0c7064a0c08b5e678b02F37) (impl) - ProxyAdmin [`0x490E441352635aacA64224c8205636FD9d2e3362`](https://testnet.bscscan.com/address/0x490E441352635aacA64224c8205636FD9d2e3362) for CrossChainController - CrossChainExecutor: [`0x69EE990d0AADEfcbbA0F2de94E0F26521ae680ff`](https://testnet.bscscan.com/address/0x69EE990d0AADEfcbbA0F2de94E0F26521ae680ff) - CCIPAdapterTestnet: [`0x39B321FC78B96fB184191788dD87e8B7c498bcEa`](https://testnet.bscscan.com/address/0x39B321FC78B96fB184191788dD87e8B7c498bcEa) - HyperLaneAdapter: [`0xa75A4F7E70a983b7388CcAA1F6C88BebC4AFc0Ef`](https://testnet.bscscan.com/address/0xa75A4F7E70a983b7388CcAA1F6C88BebC4AFc0Ef) - LayerZeroAdapterTestnet: [`0xa950B68BDA44419683c788C5E5845abC8F1863C1`](https://testnet.bscscan.com/address/0xa950B68BDA44419683c788C5E5845abC8F1863C1) - WormholeAdapterTestnet: [`0x30dF46cF148Df5eB53eb8B81b0BD5Bc785001E12`](https://testnet.bscscan.com/address/0x30dF46cF148Df5eB53eb8B81b0BD5Bc785001E12) ###### wstETH on BSC endpoints - WstEthL2Token: [`0x0B15635FCF5316EdFD2a9A0b0dC3700aeA4D09E6`](https://testnet.bscscan.com/address/0x0B15635FCF5316EdFD2a9A0b0dC3700aeA4D09E6) (proxy) - WstEthL2Token: [`0x83bc41aae95b447134e72892ba659d6ea664d496`](https://testnet.bscscan.com/address/0x83bc41aae95b447134e72892ba659d6ea664d496) (impl) - NTT Manager: [`0x66Cb5a992570EF01b522Bc59A056a64A84Bd0aAa`](https://testnet.bscscan.com/address/0x66Cb5a992570EF01b522Bc59A056a64A84Bd0aAa) (proxy) - NTT Manager: [`0xa0310f52f4ac9c394a82b2e19267a78d3390a16f`](https://testnet.bscscan.com/address/0xa0310f52f4ac9c394a82b2e19267a78d3390a16f) (impl) - Wormhole Transceiver: [`0x3a84364d27Ed3D16022Da0f603f3E0F74826c707`](https://testnet.bscscan.com/address/0x3a84364d27Ed3D16022Da0f603f3E0F74826c707) - Axelar Transceiver: [`0xaa8267908e8d2BEfeB601f88A7Cf3ec148039423`](https://testnet.bscscan.com/address/0xaa8267908e8d2BEfeB601f88A7Cf3ec148039423) - Transceiver Structs: [`0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b`](https://testnet.bscscan.com/address/0xf0396a8077eda579f657B5E6F3c3F5e8EE81972b) ### Zircuit ##### Ethereum part - L1ERC20TokenBridge: [`0x130424c81a7d497Efa53bc71BB8B718202087726`](https://sepolia.etherscan.io/address/0x130424c81a7d497Efa53bc71BB8B718202087726) (proxy) - L1ERC20TokenBridge: [`0x0b72F930bb0e378b19E93eBadf1c563D28A584ed`](https://sepolia.etherscan.io/address/0x0b72F930bb0e378b19E93eBadf1c563D28A584ed) (impl) ##### Zircuit part - WstETH ERC20Bridged: [`0x6b8116B41bFd7e1A976cB892acB79926080A6Ca1`](https://explorer.testnet.zircuit.com/address/0x6b8116B41bFd7e1A976cB892acB79926080A6Ca1) (proxy) - WstETH ERC20Bridged: [`0x549aF13787A46eF63341c8C7e78691F4a2bFbE48`](https://explorer.testnet.zircuit.com/address/0x549aF13787A46eF63341c8C7e78691F4a2bFbE48) (impl) - L2ERC20TokenBridge: [`0x7721F53d153Ae3CF937605fF1Bbb7D51B14E7902`](https://explorer.testnet.zircuit.com/address/0x7721F53d153Ae3CF937605fF1Bbb7D51B14E7902) (proxy) - L2ERC20TokenBridge: [`0x247f56cFc9021aeC161a4366412636ea33101D2B`](https://explorer.testnet.zircuit.com/address/0x247f56cFc9021aeC161a4366412636ea33101D2B) (impl) - Optimism Governance Bridge Executor: [`0x989CD486c02bfBe5c2D3C157cDCab099134e7697`](https://explorer.testnet.zircuit.com/address/0x989CD486c02bfBe5c2D3C157cDCab099134e7697) ### Soneium ##### Ethereum part - OpStackTokenRatePusher: [`0xd0E719f172430c2B22fA6252a0217469bCCecBB9`](https://sepolia.etherscan.io/address/0xd0E719f172430c2B22fA6252a0217469bCCecBB9) - L1LidoTokensBridge: [`0x3982e730E1813FA385331e491bb75c42Ab07d780`](https://sepolia.etherscan.io/address/0x3982e730E1813FA385331e491bb75c42Ab07d780) (proxy) - L1LidoTokensBridge: [`0x84f32114140a3313147291281648AaC037Bbe4B4`](https://sepolia.etherscan.io/address/0x84f32114140a3313147291281648AaC037Bbe4B4) (impl) ##### Soneium part - WstETH ERC20BridgedPermit: [`0xf7489b8d220DCf33bAe6b594C070061E4da9fDa9`](https://soneium-minato.blockscout.com/address/0xf7489b8d220DCf33bAe6b594C070061E4da9fDa9) (proxy) - WstETH ERC20BridgedPermit: [`0x1DDcF5BDc10a7d47537E4e2C8FD82c7b7EDF2fcd`](https://soneium-minato.blockscout.com/address/0x1DDcF5BDc10a7d47537E4e2C8FD82c7b7EDF2fcd) (impl) - StETH ERC20RebasableBridgedPermit: [`0x4e55E2d4c83df2E0083f1D616AFf007ac420b110`](https://soneium-minato.blockscout.com/address/0x4e55E2d4c83df2E0083f1D616AFf007ac420b110) (proxy) - StETH ERC20RebasableBridgedPermit: [`0x2746640A14A77e68167E3B51eC6ee348A23473ab`](https://soneium-minato.blockscout.com/address/0x2746640A14A77e68167E3B51eC6ee348A23473ab) (impl) - TokenRateOracle: [`0xDBD42a02D4DE52A58fe005dE2DD88B3379a50165`](https://soneium-minato.blockscout.com/address/0xDBD42a02D4DE52A58fe005dE2DD88B3379a50165) (proxy) - TokenRateOracle: [`0x116DbC8E6742d9506349a70F1A9ddB89844E9B11`](https://soneium-minato.blockscout.com/address/0x116DbC8E6742d9506349a70F1A9ddB89844E9B11) (impl) - L2ERC20ExtendedTokensBridge: [`0xc58bAe09a7A681555D7cE0F71d5b14792aca9825`](https://soneium-minato.blockscout.com/address/0xc58bAe09a7A681555D7cE0F71d5b14792aca9825) (proxy) - L2ERC20ExtendedTokensBridge: [`0x2bda94eD0d580758DC03641E0710D07f5Ac791Bd`](https://soneium-minato.blockscout.com/address/0x2bda94eD0d580758DC03641E0710D07f5Ac791Bd) (impl) - Governance Bridge Executor: [`0xded8560057e5AAb75803d440Fff46fF22Dd98cfE`](https://soneium-minato.blockscout.com/address/0xded8560057e5AAb75803d440Fff46fF22Dd98cfE) --- # 1inch Rewards Claim This is how to claim 1inch stETH/LDO pool rewards with Etherscan UI. Rewards distributed to LP on [1inchย stETH/LDO pool](https://etherscan.io/address/0x1f629794b34ffb3b29ff206be5478a52678b47ae) proportional to the amount of liquidity and timespan of providing it as described in theย [proposal](https://research.lido.fi/t/proposal-ldo-incentives-to-liquidity-providers-on-ldo-steth-pair-on-1inch-exchange/274). ## Reward claiming ### 1. Check if you are eligible to claim the reward Find your address [here](https://github.com/lidofinance/airdrop-data/blob/main/oneinch_lido_airdrop.csv) and get your index. If your address is not listed [here](https://github.com/lidofinance/airdrop-data/blob/main/oneinch_lido_airdrop.csv) you are not eligible to claim the reward. ### 2. Check if you havenโ€™t already claimed your reward 2.1 Go to [Etherscan](https://etherscan.io/address/0xdB46C277dA1599390eAb394327602889E9546296) (contract address - [0xdB46C277dA1599390eAb394327602889E9546296](https://etherscan.io/address/0xdB46C277dA1599390eAb394327602889E9546296)) 2.2 Paste your index on `isClaimed` method (1 row on [โ€œContract/Read contractโ€](https://etherscan.io/address/0xdB46C277dA1599390eAb394327602889E9546296#readContract) tab) 2.3 Press the โ€œQueryโ€ button 2.4 Make sure that the method result is `false` :::note if you get `true` as a result of this step, it means that this reward was claimed earlier, and you canโ€™t claim it once again ::: ### 3. Claim your reward 3.1 Open [โ€œContract/Write contractโ€](https://etherscan.io/address/0xdB46C277dA1599390eAb394327602889E9546296#writeContract) tab on Etherscan 3.2 Connect your wallet to Etherscan with either MetaMask or WalletConnect 3.3 Fill-in `Claim` method fields with data from [here](https://github.com/lidofinance/airdrop-data/blob/main/oneinch_lido_airdrop.csv) - index (uint256) - account (address) - amount (uint256) - merkleProof (bytes32[]) 3.4 Press the โ€œWriteโ€ button and confirm the transaction in your wallet 3.5 Wait for the transaction to succeed :::note in case of invalid input transaction can be reverted ::: That's it! ๐Ÿ’ช๐ŸŽ‰๐Ÿ --- # Multisig Signer Address Verification Using EOA across Lido DAO operational multisigs or protocol contracts requires providing a public "proof of ownership". Main use-cases here are using address as a signer in Lido DAO operational multisigs or using EOAs for offchain tooling where specific rights might be required. ## Preparing and sharing address & signature ### In case of using externally owned account (EOA) 1. Sign the message along the lines of `@my_social_handle is looking to join X Lido DAO multisig with address 0x...` with the private key you're looking to use as signing key. Use one of the following options: ### Etherscan UI (primary option) 1. Go to https://etherscan.io/verifiedSignatures and click "Sign Message" button. 2. Connect your wallet to Etherscan in the popped up window (approve the connection in your wallet app if needed). 3. Enter the message, click "Sign Message" and sign the message on the wallet. 4. Click on "Publish" button to get a link to your public verification to be used in next steps. ### MyEtherWallet (backup option) 1. Connect your wallet to https://www.myetherwallet.com/wallet/access. 2. Go to https://www.myetherwallet.com/wallet/sign (UI link is under "Message" dropdown on the left). 3. Enter the message, click "sign" and sign the message on the wallet. 4. The `sig` field in the result json is the signature hash. 2. Publish the message along with the Etherescan link (or signature hash in case of MyEtherWallet) on X.com (formerly Twitter) or other easily accessible social media. 3. Share the link to the post as a comment at the relevant [Lido DAO forum](https://research.lido.fi) post. 4. Make sure to follow the [general rules of thumb](/guides/multisig-signer-manual/) for being a signer in Lido DAO operational multisigs. ### In case of using Safe multisig 1. In https://app.safe.global home screen of your multisig wallet hit the button "New transaction" and select "Contract interaction" in the appeared screen. 2. At the New Transaction screen toggle "Custom data" switch. 3. Fill any EOA address (for example `0x0000000000000000000000000000000000000000`) into "Enter Address or ENS Name" field. 4. Use any hex encoder (like https://www.duplichecker.com/hex-to-text.php) to encode a message that consists info about who is joining what Lido committee or multisig with which address, for example `@my_social_handle is looking to join X Lido DAO multisig with address 0x...`. 5. Paste a code generated at the previous step into "Data (Hex encoded)" field of "New Transaction" screen in the multisig interface (add "0x" in the start of a HEX code if it's missing), put "0" in the ETH value field. 6. Publish the message along with the transaction hash on twitter or other easily accessible social media. 7. Share the transaction hash in the post as a comment at the relevant [Lido DAO forum](https://research.lido.fi) post. ## Ethereum signature verification ### In case of using EOA To verify the shared signature one can use Etherscan or MyEtherWallet UIs. ### Etherscan UI 1. Go to https://etherscan.io/verifiedSignatures. 2. Click `Verify Signature` button. 3. Input address, message & signature hash data & click `Continue`. 4. See whether the signature provided is valid. ### MyEtherWallet 1. Go to https://www.myetherwallet.com/tools?tool=verify. 2. Encode the message text as hex string (use the tool like https://appdevtools.com/text-hex-converter). 3. Enter json & click `Verify`: ``` { "address": "0x...", "msg": "0x...", "sig": "signature_hash" } ``` Note that "msg" is hex text starting with `0x` (add `0x` before the hex encoded string if necessary). 4. See whether the signature provided is valid. ### In case of using Safe multisig 1. Go to the signed transaction at the [Etherscan](https://etherscan.io/). 2. Click to show more details and find "input Data" field, click on "Decode input data". 3. Copy a hex code in the "data" row and take it to any hex decoder (like [duplichecker](https://www.duplichecker.com/hex-to-text.php)). 4. Decode and verify the message (please note, that you may need to delete leading `0x` from the hex code acquired in the previous step). --- # Aragon Vote: Checking the EVM Script We've published a short Replit from the script parts we're using for preparing the votes: [EVMVoteScriptParser#main.py](https://replit.com/@VictorSuzdalev/EVMVoteScriptParser#main.py) ## Checking the EVM script 1. Start Replit.![](https://user-images.githubusercontent.com/4445523/149335803-4b7c71e2-12a1-4c48-973c-c064ffa4d0a7.jpeg) 1. Open the [Replit script](https://replit.com/@VictorSuzdalev/EVMVoteScriptParser#main.py) 2. Click the big green `RUN` button at the top. 3. The script will start installing dependencies โ€” this takes a couple of minutes. 2. Get the EVM script from the vote.![](https://user-images.githubusercontent.com/4445523/149335811-1332324b-b1ba-4e4a-af2e-9c79c347ff43.jpeg) 1. Open voting contract on etherscan [0x2e59A20f205bB85a89C53f1936454680651E618e#readProxyContract](https://etherscan.io/address/0x2e59A20f205bB85a89C53f1936454680651E618e#readProxyContract) (can check the voting contract address in [Deployed contracts](/deployed-contracts/#dao-contracts)). 2. Check the `getVote` method (sixth in the list): enter the vote in question, push `query`. 3. Copy the `script` text (long string starting with 0x). 3. Check the script 1. Get back to Replit, wait for the setup to pass. 2. The Replit will ask for the EVM script โ€” paste the text from Etherscan and push `enter` to see the actions in the script. ![](https://user-images.githubusercontent.com/4445523/149335822-1bdc0c66-18f0-43c3-b2cf-124f3706ae36.png) ![](https://user-images.githubusercontent.com/4445523/149335833-3701273a-cb7a-4076-91c7-93cde4d2db4c.png) That's it! ๐Ÿ’ช๐ŸŽ‰๐Ÿ ## How to check the Replit itself - One can compare the parsing results for already passed votes with descriptions on the Voting UI ([vote #172](https://dao.lido.fi/vote/172) may be a cool example) - The Replit code is available under the `Show files` button on the left; it's heavily based on the scripts & tooling from the [scripts](https://github.com/lidofinance/scripts) repo --- # Gnosis Multisig Verification Gnosis multisig contracts are usually deployed from the Gnosis factory contracts. Gnosis has the list of `proxy_factory` contracts addresses deployed to different networks โ€” https://github.com/safe-global/safe-deployments/tree/main/src/assets ## How to verify my multisig is deployed from the Gnosis factory 1. Pick the contract version in gnosis UI (settings โ†’ safe details) โ€” those usually are `1.0.0`, `1.1.1`, `1.2.0`, or `1.3.0`. 2. Open the safe address in Network Explorer 3. Find the safe creation transaction (should be the oldest one in the "Internal Transactions" tab and have "Contract Creation" note) 4. Get the address that the safe creation transaction went to โ€” should be a factory contract 5. Open the corresponding version's folder on GitHub https://github.com/safe-global/safe-deployments/tree/main/src/assets, open the `proxy_factory.json` file, and find the address in the list of deployed addresses --- # Execution Layer Rewards Configuration Node Operators who run validators for Lido are required to set the fee recipient for the relevant validators to the protocol-managed [`LidoExecutionLayerRewardsVault`](/contracts/lido-execution-layer-rewards-vault/) which manages [Execution Layer Rewards](/contracts/lido/#gettotalelrewardscollected). This address differs depending on the network (Mainnet, testnet, etc.) and is _not_ the same as the [Withdrawal Credentials](/contracts/staking-router/#getwithdrawalcredentials) address. This smart contract address can also be retrieved by [querying the `elRewardsVault()`](/contracts/lido-locator/#elrewardsvault) method in the `LidoLocator` contract. The address is also available in the [Deployed Contracts](/deployed-contracts/) docs page, labeled as `Execution Layer Rewards Vault`. ## Fee recipient options for various Beacon Chain clients Beacon chain clients offer a variety of methods for configuring the fee recipient. For some clients the fee recipient option should be applied with other options, see reference pages for specific client. Please note that most clients also support setting the fee recipient on a per-validator key basis (e.g. for Teku this can be achieved via [the proposer config](https://docs.teku.consensys.net/en/latest/Reference/CLI/CLI-Syntax/#validators-proposer-config). Consult the docs for each client for specific instructions. | Consensus client | CLI option | CLI reference page | | ---------------- | ------------------------------------------------------- | --------------------------------- | | Teku | `--validators-proposer-default-fee-recipient=
` | [Teku CLI options] | | Lighthouse | `--suggested-fee-recipient=
` | [Lighthouse Fee Recipient Config] | | Nimbus | `--suggested-fee-recipient=
` | [Nimbus Fee Recipient Info] | | Prysm | `--suggested-fee-recipient=
` | [Prysm CLI options] | | Lodestar | `--chain.defaultFeeRecipient=
` | [Lodestar CLI options] | [teku cli options]: https://docs.teku.consensys.net/en/latest/Reference/CLI/CLI-Syntax/#validators-proposer-default-fee-recipient [nimbus fee recipient info]: https://nimbus.guide/suggested-fee-recipient.html [lighthouse fee recipient config]: https://lighthouse-book.sigmaprime.io/suggested-fee-recipient.html?highlight=fee%20recipient#suggested-fee-recipient [lodestar cli options]: https://chainsafe.github.io/lodestar/reference/cli/ [prysm cli options]: https://docs.prylabs.network/docs/execution-node/fee-recipient ## MEV-Boost related options for various Beacon Chain clients | Consensus client | CLI option | CLI reference page | | ---------------- | ---------------------------------------------------- | ---------------------------- | | Teku | `--builder-endpoint=` | [Teku MEV integration] | | Lighthouse | BN: `--builder=` VC: `--builder-proposals` | [Lighthouse MEV integration] | | Nimbus | `--payload-builder=true --payload-builder-url=` | [Nimbus MEV integration] | | Prysm | `--http-mev-relay=` | [Prysm MEV integration] | | Lodestar | BN: `--builder --builder.urls=` VC: `--builder` | [Lodestar MEV integration] | [teku mev integration]: https://docs.teku.consensys.net/en/latest/Reference/CLI/CLI-Syntax/#builder-endpoint [nimbus mev integration]: https://nimbus.guide/external-block-builder.html [lighthouse mev integration]: https://lighthouse-book.sigmaprime.io/builders.html [lodestar mev integration]: https://chainsafe.github.io/lodestar/run/beacon-management/mev-and-builder-integration/ [prysm mev integration]: https://docs.prylabs.network/docs/prysm-usage/parameters ## Relays and MEV-Boost options List of possible relays that have been approved by DAO can be fetched by [querying the `get_relays()`](/contracts/mev-boost-relays-allowed-list/#get_relays) method in `MevBoostRelayAllowedList` contract. ### Mainnet ```shell ./mev-boost -mainnet -relay-check -relay ``` ### Hoodi ```shell ./mev-boost -hoodi -relay-check -relay ``` Full list of MEV-boost CLI options can be found here [MEV-Boost CLI Options] [mev-boost cli options]: https://github.com/flashbots/mev-boost#mev-boost-cli-arguments --- # Exit Message Generation & Signing ## Keystores or Dirk If your validator signing keys are in [keystores](https://eips.ethereum.org/EIPS/eip-2335) or in [Dirk](https://github.com/attestantio/dirk) remote keymanager, the easiest method is to use [ethdo](https://github.com/wealdtech/ethdo). ### For Keystores: 1. Create an ethdo wallet 2. Import keystores 3. Generate an exit 4. Erase the wallet if it's no longer needed Create a new wallet: ```bash ./ethdo --base-dir=./temp wallet create --wallet=wallet ``` Add key from a keystore: ```bash ./ethdo --base-dir=./temp account import --account=wallet/account --keystore=./ethdo/keystore.json --keystore-passphrase=12345678 --passphrase=pass ``` Generate and sign an exit message: ```bash ./ethdo --base-dir=./temp validator exit --account=wallet/account --passphrase=pass --json --connection=http://consensus_node:5052 ``` ethdo will print out the exit message to stdout. You can save the file `ethdo ... > 0x123.json`. After we are done, delete the wallet: ```bash ./ethdo --base-dir=./temp wallet delete --wallet=wallet ``` If you are looking for a way to automate the process, check out [this example](https://gist.github.com/kolyasapphire/d2bafce3cdd04305bc109cbd49728ffe). :::info Although keystores are encrypted, it is highly recommended to interact with them in a secure environment without internet access. ::: ethdo allows you to prepare everything necessary for offline exit message generation in one convenient file. For this, on a machine with access to a Consensus Node run: ```bash ./ethdo validator exit --prepare-offline --connection=http://consensus_node:5052 --timeout=300s ``` This command will pull validators info, fork versions, current epoch and other chain data for offline exit message generation and save it to `offline-preparation.json` in the `ethdo` directory. This file can be then transferred to a secure machine along with `ethdo` binary, for example on a encrypted USB drive. On the secure machine, put `offline-preparation.json` into the directory `ethdo` is ran from, use `--offline` argument for the `validator exit` command and remove `--connection`: ```bash ./ethdo --base-dir=./temp validator exit --account=wallet/account --passphrase=pass --json --offline ``` ### For Dirk: ```bash ./ethdo --remote=server.example.com:9091 --client-cert=client.crt --client-key=client.key --server-ca-cert=dirk_authority.crt validator exit --account=Validators/1 --json --connection=http://127.0.0.1:5051 ``` [ethdo](https://github.com/wealdtech/ethdo) [ethdo Docs](https://github.com/wealdtech/ethdo/blob/master/docs/usage.md#exit) ## For Web3Signer or Proprietary Signers If you are using the `/api/v1/modules/{module_id}/validators/generate-unsigned-exit-messages/{operator_id}` endpoint of the KAPI, you can skip getting the epoch and constructing an unsigned exit message in the example below. Get current epoch: ```javascript const blockReq = await fetch(CONSENSUS_BLOCK_ENDPOINT) const blockRes = await blockReq.json() const blockNumber = blockRes.data.message.slot const currentEpoch = Math.floor(blockNumber / 32) ``` Get fork parameters: ```javascript const forkReq = await fetch(CONSENSUS_FORK_ENDPOINT) const forkRes = await forkReq.json() const fork = forkRes.data ``` Get genesis parameters: ```javascript const genesisReq = await fetch(CONSENSUS_GENESIS_ENDPOINT) const genesisRes = await genesisReq.json() const genesis_validators_root = genesisRes.data.genesis_validators_root ``` Construct an exit message: ```javascript const voluntaryExit = { epoch: String(currentEpoch), validator_index: String(VALIDATOR_INDEX), } ``` Prepare a signing request: ```javascript const body = { type: 'VOLUNTARY_EXIT', fork_info: { fork, genesis_validators_root, }, voluntary_exit: voluntaryExit, } ``` Send the request: ```javascript const signerReq = await fetch(WEB3SIGNER_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body), }) const signature = await signerReq.text() ``` Finally, construct a signed exit message: ```javascript const signedMessage = { message: voluntaryExit, signature, } ``` [Complete Example](https://gist.github.com/kolyasapphire/53dbdab35f1a033b0d37ddf582dce414) :::info It's advised to prepare all the necessary parameters (forks, epoch, etc) ahead of time and communicate with Web3Signer securely, for example via a secure network with no other internet access. ::: [Web3Signer API Docs](https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Signing) --- # Flow Examples ## With Lido Tooling (KAPI + Ejector) [![](https://hackmd.io/_uploads/Hkl5aS7x2.jpg)](https://hackmd.io/_uploads/Hkl5aS7x2.jpg) Using the recommended tooling, the flow looks like this: 1. Get a list of validators for which to generate and sign exit messages - KAPI 2. Generate and sign exit messages: - keystores - ethdo - dirk - ethdo - web3signer or a proprietary signer - custom script/tooling 3. Encrypt the message files using the Ejector encryptor script 4. Add files to the Ejector 5. Wait until valid Ejector messages are running out 6. Repeat ## Ejector Only [![](https://hackmd.io/_uploads/H1_Z4Creh.jpg)](https://hackmd.io/_uploads/H1_Z4Creh.jpg) 1. Get a list of validators for which to generate and sign exit messages: - By the order keys are stored in (eg choose oldest) - Query [NodeOperatorsRegistry](https://github.com/lidofinance/core/blob/master/contracts/0.4.24/nos/NodeOperatorsRegistry.sol) contract to get all your keys, sort by index, start with the lowest indexes. Each batch, either track the last pre-signed index or query validator status on the Consensus Node to ignore exiting and already exited validators. 2. Generate and sign exit messages: - keystores - ethdo - dirk - ethdo - web3signer or a proprietary signer - custom script/tooling 3. Encrypt the message files using the Ejector encryptor script 4. Add files to the Ejector 5. Wait until valid Ejector messages are running out 6. Repeat ## Without Lido Tooling [![](https://hackmd.io/_uploads/rJZ5TBme3.jpg)](https://hackmd.io/_uploads/rJZ5TBme3.jpg) 1. Monitor `ValidatorExitRequest` events of the [`ValidatorsExitBusOracle`](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol) 2. Generate and sign exit messages: - keystores - ethdo - dirk - ethdo - web3signer or a proprietary signer - custom script/tooling 3. Submit the messages: - ethdo can do it straight away in the previous step by leaving out `--json` argument - Submit it manually to the Consensus Node: [API Docs](https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolVoluntaryExit) --- # General Information ## What Are Exit Messages? To initiate a validator exit, an [exit message](https://github.com/ethereum/consensus-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#voluntaryexit) needs to be generated, signed and submitted to a Consensus Node. It looks like this: ```json { "message": { "epoch": "123", "validator_index": "123" }, "signature": "0x123" } ``` After it's generated, it is signed using the validator BLS key which needs to be exited and a [signed exit message](https://github.com/ethereum/consensus-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#signedvoluntaryexit) is formed. ## Pre-sign or Not to Pre-sign In short, it's a well balanced solution allowing to achieve meaningful automation without sacrificing security or decentralisation. You can find the reasoning behind the suggested approach in the [Lido Withdrawals: Automating Validator Exits RFC](https://hackmd.io/@lido/BkxRxAr-o). However, if you have existing tooling in place to create and send out exit messages, you can use webhook mode of the Ejector which will call an endpoint in order to initiate a validator exit. On the endpoint, JSON will be POSTed with the following structure: ```json { "validatorIndex": "123" "validatorPubkey": "0x123" } ``` 200 response will be counted as a successful exit, non-200 as a fail. If it's not enough, you'll have to fork the Ejector or monitor `ValidatorExitRequest` events of the [`ValidatorsExitBusOracle`](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol) in your tooling. [Example in the Ejector](https://github.com/lidofinance/validator-ejector/blob/d72cac9767a57936f29c5b54e7de4f74344342de/src/services/execution-api/service.ts#L160-L203) ## How Many Keys to Pre-sign Node Operators should pre-sign an amount or a percentage of validators which they are comfortable with managing. Smaller amounts means more frequent refills and vice versa. For Withdrawals preparations, for example, exits for 10% of the validators pre-signed is suggested. After that, it will depend on the Withdrawals demand and Node Operator preferences. ## How to Understand Which Keys to Pre-sign ### Using KAPI (recommended) First endpoint returns a list of validators for a specific Node Operator, for which to generate and sign exit messages next: `/v1/modules/{module_id}/validators/validator-exits-to-prepare/{operator_id}` Returns data: ```json [{ "validatorIndex": 123; "key": "0x123"; }] ``` Additionally, there is also a second endpoint which calculates the same data, but returns ready to sign exit messages: `/v1/modules/{module_id}/validators/generate-unsigned-exit-messages/{operator_id}` :::danger Make sure to visually inspect the returned data as a precaution as you will be signing it. You can find the expected format in the [What Are Exit Messages](#what-are-exit-messages) section. ::: Returns data: ```json [{ "validator_index": "123"; "epoch": "123"; }] ``` Furthermore, both endpoints allow for additional configuration via query parameters: - `percent` - Percent of validators to return data for. Default value is 10. - `max_amount` - Number of validators to return data for. If validator number is less than the specified amount, all validators are returned. :::info Note: Only one parameter is active at a time. If both are provided, `percent` has a higher priority. ::: KAPI will automatically filter out validators which are exited already or are currently exiting, so one call to the KAPI is all that's needed. ### Manually If your validator signing keys are stored in generation order, you can simply start exit generation and signing from the oldest keys since the exit algorithm is deterministic and will choose oldest keys first for each Node Operator. However, for each batch you'll need to either track the last key exit message was generated for or query validator statuses on the Consensus Node to understand where to start next. ## Storing Signed Exit Messages Storing signed exit messages as-is is discouraged. If an attacker gains access to them, they will simply submit them, which will exit every validator for which you had exit messages. It's recommended to encrypt the messages, for example using the encryption script provided with the Ejector. [How to Use the Ejector Encryptor](https://hackmd.io/@lido/BJvy7eWln#Encrypting-Messages) You can also check out the [source code](https://github.com/lidofinance/validator-ejector/blob/develop/encryptor/encrypt.ts) and integrate it in your own tooling if needed. Ejector automatically decrypts [EIP-2335](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2335.md)-encrypted files on app start. ## How to Initiate a Validator Exit Signed messages are sent to a Consensus Node via the `/eth/v1/beacon/pool/voluntary_exits` endpoint in order to initiate an exit. The Ejector will do this automatically when necessary. If you don't run the Ejector, you'll have to do this manually or develop your own tooling. If your exit messages are encrypted, you'll need to decrypt them first. --- # Introduction [Lido V2](https://blog.lido.fi/introducing-lido-v2/) protocol upgrade adds support for [Ethereum Withdrawals](https://ethereum.org/en/staking/withdrawals/) and introduces additional responsibilities for Node Operators. Lido Withdrawals are happening in four stages: 1. stETH holders request a withdrawal 2. Lido Oracles decide which Lido validators should be exited to fulfil the request and publish the list on-chain 3. Lido Node Operators exit these validators 4. stETH holders claim their ETH This means that Node Operators need a way to react quickly to protocol requests. The suggested method is to generate and sign exit messages ahead of time which will be sent out when needed by a special new daemon called the Validator Ejector. To understand for which validators to generate and sign exit messages, another new app called Keys API is introduced. ## Requirements First, is running Lido tooling required? Lido tooling is not required to use, but is recommended. The only requirement for Node Operators is to exit their validators in time after requested by the protocol. For details, check out Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)). ## Lido Tooling ### Keys API (KAPI for short) KAPI is a service which stores and serves up-to-date information about Lido validators. It provides a very important function: it provides two endpoints, using which a Node Operator understands for which validators to generate and sign exit messages in advance. Under the hood, KAPI also automatically filters out validators which are exited already or are currently exiting. ### Validator Ejector (Ejector for short) Ejector is a daemon service which monitors `ValidatorsExitBusOracle` events and initiates an exit when required. In messages mode, on start, it loads exit messages in form of individual .json files from a specified folder or an external storage and validates their format, structure and signature. It then loads events from a configurable amount of latest finalized blocks, checks if exits should be made and after that periodically fetches fresh events. In webhook mode, it simply fetches a remote endpoint when an exit should be made, allowing to implement JIT approach by offloading exiting logic to an external service and using the Ejector as a secure exit events reader. --- # Tooling Setup & Configuration ## Keys API (KAPI) [Dedicated Setup Guide](https://hackmd.io/@lido/S1Li-wXl3) [GitHub Repo](https://github.com/lidofinance/lido-keys-api) ## Validator Ejector (Ejector) [Dedicated Setup Guide](https://hackmd.io/@lido/BJvy7eWln) [GitHub Repo](https://github.com/lidofinance/validator-ejector) ## Required Infra for New Tooling In order for the new tooling to read Lido contracts and validator information, tooling needs access to an Execution Node (full node to be exact) and a Consensus Node. A dedicated CL+EL setup is recommended. :::info Although the Ejector has [security protections](https://github.com/lidofinance/validator-ejector#safety-features), using hosted RPC providers (Infura, Alchemy, etc) is discouraged. ::: :::info It's also advised to have secure Ejector->Nodes and KAPI->Nodes communication, for example via a private network. ::: ## Common Configuration Options ### Operator ID You can find it on the Operators Dashboard (`#123` on the operator card): [Hoodi](https://operators-hoodi.testnet.fi), [Mainnet](https://operators.lido.fi) ### Staking Router Module ID: ID of the [StakingRouter](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/StakingRouter.sol) contract module. Currently, it has only one module ([NodeOperatorsRegistry](https://github.com/lidofinance/core/blob/master/contracts/0.4.24/nos/NodeOperatorsRegistry.sol)), it's id is `1`. ### Oracle Allowlist The oracle members are retrievable from the HashConsensus (for the Validator Exit Bus Oracle ) contract on-chain, directly from the contract using Etherscan. | network | Contract Call | | -------- | ------------- | | Mainnet | [getMembers()](https://etherscan.io/address/0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a#readContract#F16) | | Hoodi | [getMembers()](https://hoodi.etherscan.io/address/0x30308CD8844fb2DB3ec4D056F1d475a802DCA07c#readContract#F16) | ## Example Infra Setup Lido DevOps team prepared an easy way to get the recommended tooling and its dependencies up and running using [Ansible](https://github.com/ansible/ansible). This is a great way to get familiar with the new tooling. This is an example implementation, and still requires security and hardening by the NO; it can be found on [GitHub](https://github.com/lidofinance/node-operators-setup). It sets up 3 hosts: - Execution Layer + Consensus Layer nodes ([Geth](https://github.com/ethereum/go-ethereum) + [Lighthouse](https://github.com/sigp/lighthouse)) - KAPI & Ejector - Monitoring Monitoring consists of: - [Prometheus](https://github.com/prometheus/prometheus) for metrics - [Alertmanager](https://github.com/prometheus/alertmanager) for alerts - [Loki](https://github.com/grafana/loki) for logs - [Grafana](https://github.com/grafana/grafana) for dashboards --- # General Overview Node Operators in the Curated Module manage a secure and stable infrastructure for running Beacon validator clients for the benefit of the protocol. Theyโ€™re professional staking providers who can ensure the safety of funds belonging to the protocol users and correctness of validator operations. The general flow is the following: 1. A Node Operator expresses their interest to the DAO members. Their address gets proposed to the DAO vote for inclusion to the Curated Module's Node Operator list. Note that the Node Operator address should be supplied to the DAO with zero signing keys limit. 2. The DAO votes for including the Operator to the list of active operators. After successful voting for inclusion, the Node Operator becomes active. 3. The Node Operator generates and submits a set of signing public keys and associated signatures for future validators that will be managed by the Operator. When generating the signatures, the Operator must use withdrawal credentials derived from the withdrawal address supplied by the DAO. 4. The DAO members check the submitted keys for correctness and, if everythingโ€™s good, vote for approving them. After successful approval, the keys become usable by the protocol. 5. The protocol distributes the pooled ether evenly between all active Node Operators in `32 ether` chunks. When it assigns the next deposit to a Node Operator, it takes the first non-used signing key, as well as the associated signature, from the Node Operatorโ€™s usable set and performs a deposit to the official `DepositContract`, submitting the pooled funds. At that time, the Node Operator should have the validator already running and configured with the public key being used. 6. From this point, the Node Operator is responsible for keeping the validator associated with the signing key operable and well-behaving. 7. The protocol includes Oracles that periodically report the combined Beacon balance of all validators launched by the protocol. When the balance increases as a result of Beacon chain rewards, a fee is taken from the amount of rewards (see below for the details on how the fee is denominated) and distributed between active Node Operators. 8. As withdrawals are requested, protocol publishes exit requests and Node Operators exit requested validators. ## The fee The fee is taken as a percentage from Beacon chain rewards at the moment the Oracles report those rewards. Oracles do that once in a while โ€” the exact period is decided by the DAO members via the voting process. The total fee percentage, as well as the percentage that goes to all Node Operators, is also decided by the DAO voting and can be changed during the lifetime of the DAO. The Node Operatorsโ€™ part of the fee is distributed between the active Node Operators proportionally to the number of validators that each Node Operator runs. > For example, if Oracles report that the protocol has received 10 ether as a reward, the fee > percentage that goes to Operators is `10%`, and there are two active Node Operators, running > `2` and `8` validators, respectively, then the first operator will receive `0.2` stETH, the > second โ€” `0.8` stETH. The fee is nominated in stETH, a liquid version of staked ETH introduced by the Lido protocol. The tokens correspond 1:1 to the ether that the token holder would be able get by burning their stETH if transfers were already enabled in the Beacon chain. At any time point, the total amount of stETH tokens is equal to the total amount of ether controlled by the protocol on both Execution Layer and Consensus Layer sides. When a user submits ether to the pool, they get the same amount of freshly-minted stETH tokens. When reward is received on the Consensus Layer side, each stETH holderโ€™s balance increases by the same percentage that the total amount of protocol-controlled ether has increased, corrected for the protocol fee which is taken by [minting new stETH tokens] to the fee recipients. > For example, if the reward has increased the total amount of protocol-controlled ether by `10%`, > and the total protocol fee percentage is `10%`, then each token holderโ€™s balance will grow by > approximately `9.09%`, and `10%` of the reward will be forwarded to the Lido DAO treasury and Node Operators. One side effect of this is that you, as a Node Operator, will continue receiving the percentage of protocol rewards even after you stop actively validating, if you chose to hold stETH received as a fee. [minting new steth tokens]: https://github.com/lidofinance/lido-dao/blob/971ac8f/contracts/0.4.24/Lido.sol#L576 ## Expressing interest to the DAO holders To include a Node Operator to the protocol, DAO holders must perform a voting. A Node Operator is defined by an address that is used for two purposes: 1. The protocol pays the fee by minting stETH tokens to this address. 2. The Node Operator uses this address for submitting signing keys to be used by the protocol. Pass this address to the DAO holders along with the other relevant information. ## Validator Exits Protocol, Penalties, and Recovering According to the Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)), a Node Operator participating in the Lido on Ethereum protocol are responsible for correctly exiting validators within a specified timeframe determined by the protocol's requirements and rules set by the DAO. In essence, if a Node Operator is unable to withdraw a validator within the time specified by the `VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS` parameter in the `OracleDaemonConfig` contract, the accounting oracle report for that Node Operator increases the `STUCKED` field by the number of delayed validators. Therefore, a Node Operator is penalized if they have more `STUCKED` validators than `REFUNDED` validators. While this condition is met, the Node Operator receives only half of the rewards and no new stake allocations. Once the Node Operator manages to either withdraw the required number of validators or compensate for the lost validators and increases the `REFUNDED` count through DAO voting, the Node Operator is considered under penalty for the duration of the `STUCK_PENALTY_DELAY` period and then returns to the normal state. Rewards are automatically restored to normal, but to start receiving new stake, the Node Operator (or anyone else) must call the permissionless method `clearNodeOperatorPenalty`. To clear penalty please send a transaction with desired `_nodeOperatorId`: [https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5#writeProxyContract#F2](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5#writeProxyContract%23F2) --- # Validator Keys Validator keys are added in several sequential steps. These steps are similar for each time new keys are added. ## Generating signing keys Upon inclusion into the protocol, a Node Operator should generate and submit a set of [BLS12-381] public keys that will be used by the protocol for making ether deposits to the Ethereum [DepositContract](https://etherscan.io/address/0x00000000219ab540356cBB839Cbe05303d7705Fa). Along with the keys, a Node Operator submits a set of the corresponding signatures [as defined in the spec]. The `DepositMessage` used for generating the signature must be the following: - `pubkey` must be derived from the private key used for signing the message; - `amount` must equal to 32 ether; - `withdrawal_credentials` must equal to the protocol credentials set by the DAO. ### Withdrawal Credentials Make sure to obtain correct withdrawal address by finding it inside the active withdrawal credentials by calling the contract via [`StakingRouter.getWithdrawalCredentials()`](/contracts/staking-router.md#getwithdrawalcredentials). For example withdrawal credentials `0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293f` mean that the withdrawal address is `0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f`. For Mainnet, always verify the address is correct using an [explorer] - you will see that it was deployed from the Lido deployer. [bls12-381]: https://ethresear.ch/t/pragmatic-signature-aggregation-with-bls/2105 [as defined in the spec]: https://github.com/ethereum/annotated-spec/blob/master/phase0/beacon-chain.md#depositmessage [explorer]: https://etherscan.io/address/0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f ### Using staking-deposit-cli Use the latest release of [`staking-deposit-cli`]. Example command usage: ```sh ./deposit new-mnemonic --folder . --num_validators 123 --mnemonic_language english --chain mainnet --eth1_withdrawal_address 0x123 ``` Here, `chain` is one of the available chain names (run the command with the `--help` flag to see the possible values: `./deposit new-mnemonic --help`) and `eth1_withdrawal_address` is the withdrawal address from the protocol documentation. As a result of running this, the `validator_keys` directory will be created in the current working directory. It will contain a deposit data file named `deposit-data-*.json` and a number of private key stores named `keystore-*.json`, the latter encrypted with the password you were asked for when running the command. If you chose to use the UI for submitting the keys, youโ€™ll need to pass the JSON data found in the deposit data file to the protocol (see the next section). If you wish, you can remove any other fields except `pubkey` and `signature` from the array items. Never share the generated mnemonic and your private keys with anyone, including the protocol members and DAO holders. [`staking-deposit-cli`]: https://github.com/ethereum/staking-deposit-cli/releases ## Validating the keys Please, make sure to check the keys validity before submitting them on-chain. Lido submitter has validation functionality built-in, keys will be checked before submitting. If you will be submitting keys manually via Lido contract, you can use Lido CLI. It's a Python package which you can install with pip: ```sh pip install lido-cli lido-cli --rpc http://1.2.3.4:8545 validate_file_keys --file keys.json ``` You would need an RPC endpoint - a local node / RPC provider (eg Alchemy/Infura). ## Submitting the keys > Please note, that the Node Operator's rewards address should be added to the Lido Node Operators Registry before it can submit the signing keys. Adding an address to the Node Operators Registry happens via DAO voting. When providing a rewards address to be added to the Node Operators Registry, keep the following in mind: > > - it is the address that will receive rewards; > - it is the address the NO will use to submit keys to Lido; > - the NO should be able to access it at any time in case of emergency; > - the address could be a multi-sig if that's preferred; > - changing the address will require another DAO vote. After generating the keys, a Node Operator submits them to the protocol. To do this, they send a transaction from the Node Operatorโ€™s rewards address to the `NodeOperatorsRegistry` contract instance, calling [`addSigningKeys` function] and with the following arguments: ``` * `uint256 _nodeOperatorId` the zero-based sequence number of the operator in the list; * `uint256 _keysCount` the number of keys being submitted; * `bytes _publicKeys` the concatenated keys; * `bytes _signatures` the concatenated signatures. ``` The address of the `NodeOperatorsRegistry` contract instance can be obtained by calling the [`getOperators()` function] on the `Lido` contract instance. The ABI of the `NodeOperatorsRegistry` contract can be found on the corresponding contract page on Etherscan or in `***-abi.zip` of the latest release on the [lido-dao releases github page](https://github.com/lidofinance/lido-dao/releases). Operator id for a given reward address can be obtained by successively calling [`NodeOperatorsRegistry.getNodeOperator`] with the increasing `_id` argument until you get the operator with the matching `rewardAddress`. Etherscan pages for the Hoodi contracts: - [`Lido`](https://hoodi.etherscan.io/address/0x3508A952176b3c15387C97BE809eaffB1982176a#readProxyContract) - [`NodeOperatorsRegistry`](https://hoodi.etherscan.io/address/0x5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5) Etherscan pages for the Mainnet contracts: - [`Lido`](https://etherscan.io/address/0xae7ab96520de3a18e5e111b5eaab095312d7fe84#readProxyContract) - [`NodeOperatorsRegistry`](https://etherscan.io/address/0x55032650b14df07b85bf18a3a3ec8e0af2e028d5#readProxyContract) [`getoperators()` function]: https://github.com/lidofinance/lido-dao/blob/971ac8f/contracts/0.4.24/Lido.sol#L361 [`nodeoperatorsregistry.getnodeoperator`]: https://github.com/lidofinance/lido-dao/blob/971ac8f/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L335 ### Using the batch key submitter UI Lido provides UIs for key submission: [Mainnet web interface for submitting the keys] and a [Testnet web interface for submitting the keys]. ![Submitter](/img/node-operators-manual/submitter.png) If youโ€™ve used the `staking-deposit-cli`, you can paste the content of the generated `deposit-data-*.json` file as-is. Else, prepare a JSON data of the following structure and paste it to the textarea that will appear in the center of the screen: ```json [ { "pubkey": "PUBLIC_KEY_1", "withdrawal_credentials": "WITHDRAWAL_CREDENTIALS_1", "amount": 32000000000, "signature": "SIGNATURE_1", "fork_version": "FORK_VERSION_1", "eth2_network_name": "ETH2_NETWORK_NAME_1", "deposit_message_root": "DEPOSIT_MESSAGE_ROOT_1", "deposit_data_root": "DEPOSIT_DATA_ROOT_1" }, { "pubkey": "PUBLIC_KEY_2", "withdrawal_credentials": "WITHDRAWAL_CREDENTIALS_2", "amount": 32000000000, "signature": "SIGNATURE_2", "fork_version": "FORK_VERSION_2", "eth2_network_name": "ETH2_NETWORK_NAME_2", "deposit_message_root": "DEPOSIT_MESSAGE_ROOT_2", "deposit_data_root": "DEPOSIT_DATA_ROOT_2" } ] ``` This tool will automatically split the keys into chunks and submit the transactions to your wallet for approval. Transactions will come one by one for signing. Unfortunately, we cannot send a large number of keys in a single transaction. Right now, the chunk size is 50 keys, it's close to the limit of gas per block. Connect your wallet, click `Validate` button, the interface would run required checks. And then click `Submit keys` button. We now support the following connectors: - MetaMask and similar injected wallets - Wallet Connect - Gnosis Safe - Ledger HQ If you want to use Gnosis, there are two ways to connect: - Add this app as a [custom app] in your safe. - [Use WalletConnect] to connect to your safe. When you submit a form, the keys are saved in your browser. This tool checks the new key submits against the previously saved list to avoid duplication. Therefore it is important to use one browser for submitting. [mainnet web interface for submitting the keys]: https://operators.lido.fi/submitter [testnet web interface for submitting the keys]: https://operators.testnet.fi/submitter [custom app]: https://help.safe.global/en/articles/40859-add-a-custom-safe-app [use walletconnect]: https://help.safe.global/en/articles/108235-how-to-connect-a-safe-to-a-dapp-using-walletconnect ## Importing the keys to a Lighthouse validator client If youโ€™ve used the forked `staking-deposit-cli` to generate the keys, you can import them to a Lighthouse validator client by running this command: ```sh docker run --rm -it \ --name validator_keys_import \ -v "$KEYS_DIR":/root/validator_keys \ -v "$DATA_DIR":/root/.lighthouse \ sigp/lighthouse \ lighthouse account validator import \ --reuse-password \ --network "$TESTNET_NAME" \ --datadir /root/.lighthouse/data \ --directory /root/validator_keys ``` ## Checking the keys of all Lido Node Operators Key checking works with on-chain data. Make sure key submission transactions are confirmed before checking the keys. Never vote for increasing the key limits of Node Operators before verifying new keys are present and valid. ### Lido CLI Make sure Python with pip is installed and then run: ```sh pip install lido-cli lido-cli --rpc http://1.2.3.4:8545 validate_network_keys --details ``` This operation checks all Lido keys for validity. This is a CPU-intensive process, for example, a modern desktop with 6 cores, 12 threads and great cooling processes 1k keys in 1โ€”2 seconds. You would need an RPC endpoint - a local node / RPC provider (eg Alchemy/Infura). ### Lido Node Operator Dashboard You can also check the uploaded keys on [Mainnet Lido Node Operator Dashboard] or [Hoodi Lido Node Operator Dashboard]. This UI shows a number of submitted, approved and valid keys for each Node Operator, along with all invalid keys in case there are any. It is updated every 30 minutes via cron, but update period may change in the future. [mainnet lido node operator dashboard]: https://operators.lido.fi [hoodi lido node operator dashboard]: https://operators-hoodi.testnet.fi ### Results #### You don't see invalid keys If the new keys are present and valid, Node Operators can vote for increasing the key limit for the Node Operator. #### You spot invalid keys It is urgent to notify Lido team and other Node Operators as soon as possible. For example, in the group chat. ## Increasing the Staking Limits with an Easy Track motion Once new keys are present and valid, a motion can be proposed to increase the staking limit for the Node Operator. [Node Operators Guide to Easy Track](/guides/easy-track-guide/#node-operators-guide-to-easy-track) --- # Deposit Security Committee manual This instruction has been prepared for the participants of the Deposit Security Committee and describes the general points, the preparation steps to act as a guardian, and the details of the protection mechanism. The Deposit Security Committee is necessary to prevent the substitution of withdrawal credentials with frontrunning by node operators. Each member of the committee must perform several actions to ensure the security of deposits made by Lido. To participate in the validation, you will need to deploy a `lido-council-daemon` and prepare a private key for signing messages about the correctness of data or the need to stop deposits in case of attack. ## TL;DR Before running in the mainnet all steps should be done in the Hoodi testnet. 1. Prepare an EOA account for signing data with a private key on hand (not in hardware wallet). It will be a moderately sensitive hot private key. Use different accounts for testnet and mainnet. 2. Send the account address to Lido for submitting it to the smart contract. 3. Deploy and run `lido-council-daemon` with the private key from the EOA account. It would work in a dry-run mode until your address would be included in the smart contract. ## Detailed description ### The vulnerability There is the vulnerability allowing the malicious Node Operator to intercept the user funds on deposits to the Beacon chain in the Lido protocol. The vulnerability could only be exploited by the Node Operator front-running the `Lido.depositBufferedEther` transaction with direct deposit to the DepositContract of no less than 1 ETH with the same validator public key & withdrawal credentials different from the Lidoโ€™s ones, effectively getting control over 32 ETH from Lido. To mitigate this, Lido contracts should be able to check that Node Operatorsโ€™ keys hadnโ€™t been used for malicious predeposits. ### The Deposit Security Committee The Deposit Security Committee has been established to ensure the safety of deposits on the Beacon chain: - **Monitoring and Messaging**: Monitors the history of deposits and the set of Lido keys available for deposits, signs, and disseminates messages to permit deposits. - **Pause on Malice Detection**: Signs the "pause" message that allows anyone to halt deposits when malicious Node Operator deposits are detected. - **Enhanced Security Measures**: Signs the message that enables the unvetting of keys from the Staking Module in cases of detected malicious activities, duplicate entries, or invalid keys by Node Operators. To make a deposit, we propose to collect a quorum of 4/6 of the signatures of the committee members. Members of the committee can collude with node operators and steal money by signing bad data that contains malicious predeposits. To mitigate this we propose to allow single committee member to stop deposits and also enforce space deposits in time (e.g. no more than 150 deposits with 150 blocks in between them), to provide single honest participant an ability to stop further deposits even if the supermajority colludes. The idea was outlined on research forum post as the option [d](https://research.lido.fi/t/mitigations-for-deposit-front-running-vulnerability/1239#d-approving-deposit-contract-merkle-root-7). ### Committee membership The committee consists of five node operators and the Lido dev team. The current list of guardians and their addresses is published on the [Lido Council Daemon](/holders/lido-council-daemon#mainnet-members) page. In the future, we want to bring as many node operators as possible into the mix, so the expectation will be that while the 6 guardians start the rest of the node operators can also participate via testnet and gradually get pulled into mainnet. After the [LIP-37](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) vote, a guardian seat is held by the member's `DelegationContract` under the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide), not by an EOA. The member's hot key becomes the delegate of that contract and can be rotated or revoked by the member without a governance vote. ### Members responsibilities Each member must prepare a hot key to sign the pair `(depositRoot, keysOpIndex)`. The address added to the smart contract is the member's `DelegationContract` (before the LIP-37 vote: the member's EOA), and the hot key is its delegate. The `DepositSecurityModule` verifies guardian signatures through ERC-1271, so a signature is valid only while the signing key is the active delegate of the contract. Also, members have to run `DSC Daemon` that monitors the validatorsโ€™ public keys in the `DepositContract` and in all Staking Modules. The daemon must have access to the delegateโ€™s private key to be able to perform ECDSA signing. See the [EDF Operator Guide](/guides/edf/edf-operator-guide) for the setup. ## Preparation steps Before running in the mainnet, all steps should be completed in the Hoodi testnet. ### EOA account It might be any EOA account under the member's control. Send the address of its account to Lido for submitting it to the smart contract. Lido will provide some ETH to make stopping transactions if needed (shouldn't ever be the case). Note, all actions, except sending the stop message, would be done off-chain. ### Onchain Data Bus Communication For inter-service communication, an onchain data bus is utilized, based on EVM-based network and a simple smart contract. This smart contract allows for sending messages essential for the operation of the service. The current specification of these messages is outlined in [this file](https://github.com/lidofinance/lido-council-daemon/blob/main/src/abi/data-bus.abi.json). For more details on the smart contract, please refer to [the document](/contracts/data-bus/). ### Run lido-council-daemon To start the application, see the technical documentation in the project [repository](https://github.com/lidofinance/lido-council-daemon#table-of-contents). --- # Lido depositor bot ## Introduction Depositor bot is a part of [Deposit Security Module](/contracts/deposit-security-module/). The Depositor Bot obtains signed deposit messages from Council Daemons. Once a sufficient number of messages is collected to constitute a quorum, the bot proceeds to initiate a deposit into the designated staking module. This deposit is executed using the [depositBufferedEther](/contracts/deposit-security-module/#depositbufferedether) function within the [DepositSecurityModule](/contracts/deposit-security-module) smart contract. Since v5.6.0 the bot also performs **top-ups**: adding ETH to already-active validators of `0x02` (compounding) staking modules through the `TopUpGateway` contract. Top-ups do not require a guardian quorum โ€” only the bot itself can submit them โ€” and they keep working while deposits are paused in the DSM. Top-ups are disabled by default (`ENABLE_TOP_UP=false`) and must stay disabled until Node Operators have submitted consolidation requests. The full per-iteration algorithm (module prioritisation, seed deposits vs. top-ups, validator selection) is described in [depositor-algorithm.md](https://github.com/lidofinance/depositor-bot/blob/main/docs/depositor-algorithm.md) in the bot repository. ## Requirements ### Hardware - 1-core CPU - 2GB RAM With top-ups enabled the bot fetches and decodes the full beacon state (SSZ) to build validator proofs, which needs noticeably more RAM and network bandwidth than the figures above โ€” size the host against the current validator set. ### Nodes - Ethereum EL RPC service - Ethereum CL RPC service with the debug API available (`/eth/v2/debug/beacon/states`) - [Lido Keys API](/guides/tooling/#keys-api) instance - Onchain databus transport RPC service (Gnosis at the moment) The CL node and the Keys API are only *used* by the top-up path, but the depositor bot verifies that the EL, CL and Keys API endpoints are reachable and report the same chain id on start up. It therefore requires all of them even when `ENABLE_TOP_UP=false`, and exits on start up if they are missing or unreachable. ## How to use Depositor bot performs series of checks before accepting the deposit. One of the most important optimisations it is doing is optimising gas spending. An example of this is fetching `GAS_FEE_PERCENTILE_DAYS_HISTORY_1` days of gas history and checking `GAS_FEE_PERCENTILE_1` bot will send transactions only if current gas price is less or equals to the percentile. Also `GAS_PRIORITY_FEE_PERCENTILE`, `MIN_PRIORITY_FEE`, `MAX_PRIORITY_FEE` variables are used to calculate `maxFeePerGas` and `maxPriorityFeePerGas` transaction parameters. The formula is: ``` priority = min(max( GAS_PRIORITY_FEE_PERCENTILE reward percentile of fee history for the last block, MIN_PRIORITY_FEE, ), MAX_PRIORITY_FEE, ) maxFeePerGas = baseFeePerGas * 2 + priority maxPriorityFeePerGas = priority ``` ### Envs Required variables are(mainnet): | Variable | Default | Description | |-----------------------------------|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| | WEB3_RPC_ENDPOINTS | - | List of EL rpc endpoints that will be used to send requests comma separated (`,`) | | WALLET_PRIVATE_KEY | - | Account private key | | CREATE_TRANSACTIONS | false | If true then tx will be send to blockchain | | LIDO_LOCATOR | 0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb | Lido Locator address. Mainnet by default. Other networks can be found [here](/deployed-contracts/) | | DEPOSIT_CONTRACT | 0x00000000219ab540356cBB839Cbe05303d7705Fa | Ethereum deposit contract address | | DEPOSIT_MODULES_WHITELIST | - | Comma separated list of staking module's ids in which the depositor bot will make deposits and top-ups | | --- | --- | --- | | ENABLE_TOP_UP | false | Enables top-ups of `0x02` modules. Keep disabled until Node Operators submit consolidation requests | | CL_API_URLS | - | Comma separated list of CL endpoints. Required even when `ENABLE_TOP_UP=false` | | KEYS_API_URL | - | [Keys API](/guides/tooling/#keys-api) URL. Required even when `ENABLE_TOP_UP=false` | | MAX_VALIDATORS_PER_TOP_UP | 32 | Maximum number of validators per top-up transaction (also capped onchain by the `TopUpGateway`) | | --- | --- | --- | | MESSAGE_TRANSPORTS | - | Transports used in bot. Set: onchain_transport | | ONCHAIN_TRANSPORT_RPC_ENDPOINTS | - | List of databus(Gnosis) rpc endpoints that will be used for reading data bus contract, comma separated (`,`). | | ONCHAIN_TRANSPORT_ADDRESS | - | Data bus contract address. | | MIN_PRIORITY_FEE | 50 mwei | Min priority fee that will be used in tx | | MAX_PRIORITY_FEE | 10 gwei | Max priority fee that will be used in tx | | MAX_GAS_FEE | 100 gwei | Bot will wait for a lower price. Threshold for gas_fee | | CONTRACT_GAS_LIMIT | 15000000 | Default transaction gas limit | | GAS_FEE_PERCENTILE_1 | 20 | Percentile for first recommended fee calculation | | GAS_FEE_PERCENTILE_DAYS_HISTORY_1 | 1 | Percentile for first recommended calculates from N days of the fee history | | GAS_PRIORITY_FEE_PERCENTILE | 25 | Priority transaction will be N percentile from priority fees in last block (min MIN_PRIORITY_FEE - max MAX_PRIORITY_FEE) | Optional variables can be found [here](https://github.com/lidofinance/depositor-bot/blob/main/README.md). ## Running ### Source Code 1. Clone repository and install requirements: ```bash git clone git@github.com:lidofinance/depositor-bot.git cd depositor-bot ``` 2. Install requirements ```bash poetry install ``` 3. Run depositor bot ```bash poetry run python src/main.py depositor ``` 4. Verify in logs that depositor bot is performing validations, you should see logs of a kind: ``` {"name": "bots.depositor", "levelname": "INFO", "funcName": "execute", "lineno": 210, "module": "depositor", "pathname": "/app/src/bots/depositor.py", "timestamp": 1753350000, "msg": "Depositor iteration start.", "block_number": 23000000} {"name": "bots.depositor", "levelname": "INFO", "funcName": "_execute_actual", "lineno": 245, "module": "depositor", "pathname": "/app/src/bots/depositor.py", "timestamp": 1753350000, "msg": "Depositable ether.", "value": 3200000000000000000000} {"name": "bots.depositor", "levelname": "INFO", "funcName": "_execute_actual", "lineno": 303, "module": "depositor", "pathname": "/app/src/bots/depositor.py", "timestamp": 1753350000, "msg": "Phase B start: full deposits to 0x01 + top-up to 0x02."} {"name": "bots.depositor", "levelname": "INFO", "funcName": "_deposit_to_module", "lineno": 490, "module": "depositor", "pathname": "/app/src/bots/depositor.py", "timestamp": 1753350000, "msg": "Gas price too high โ€” skip deposit.", "module_id": 1} {"name": "bots.depositor", "levelname": "INFO", "funcName": "execute", "lineno": 215, "module": "depositor", "pathname": "/app/src/bots/depositor.py", "timestamp": 1753350000, "msg": "Depositor iteration finished.", "value": true} ``` If you are facing problems, check what environment variables depositor bot is using, find a log line `"msg": "Bot env variables"` ### Docker Docker image can be found [here](/guides/tooling/#depositor-bot). ## Monitoring Prometheus metrics will be available on endpoint `http://localhost:${PROMETHEUS_PORT}/metrics`. The metrics list is defined in [src/metrics/metrics.py](https://github.com/lidofinance/depositor-bot/blob/main/src/metrics/metrics.py). Alerts [source code](https://github.com/lidofinance/depositor-bot/blob/main/alerts/alerts.yml) for AlertManager. --- # Guide to Dual Governance [The Dual Governance interface](https://dao.lido.fi/dg/) provides stETH holders with tools to monitor the governance state, escrow tokens for Veto Signaling, manage them during Veto Signaling and withdraw after a Rage Quit. For more information about Dual Governance [visit the blog](https://blog.lido.fi/dual-governance-101-explainer/) or check [the specification](https://github.com/lidofinance/dual-governance/blob/3e0f1ae5740ef8410e928f6cc106e3a5f45a5a75/docs/specification.md). ## Understanding Governance State 1. Visit [https://dao.lido.fi/dg](https://dao.lido.fi/dg/) 2. Ongoing proposals and their status are listed on the right side of the block. To learn more about the proposals, scroll down and click for more details. ![](/img/dg-guide/state_00.png) 3. The current governance state is displayed on the left side of the block. **Normal state**: A proposal can be submitted to Dual Governance. After the default 3-day timelock, the proposal becomes executable. ![](/img/dg-guide/state_01.png) *The background turns yellow when 30% of the first threshold (Veto Signalling, 1% of total stETH supply) is reached, though the governance state remains Normal.* ![](/img/dg-guide/state_02.png) **Veto Signalling**: Governance motions are blocked for 5 to 45 days, depending on the amount of opposing tokens. Submission of new proposals remains active during this period. To find out details and join the ongoing discussion, click on the **Public Report** link. ![](/img/dg-guide/state_03.png) **Rage Quit:** Governance motions are blocked until tokens in the escrow exit the protocol. Submission of new proposals remains active during this period. ![](/img/dg-guide/state_04.png) **Deactivation**: A brief period indicating Veto Signalling is about to end, after which non-cancelled proposals will be available for execution in the next state. Proposal submission is blocked during Deactivation. ![](/img/dg-guide/state_05.png) **Cooldown:** Transitional state after Veto Signalling or Rage Quit when pending proposals can be executed even if the opposition is higher than 1% of the total stETH supply. ![](/img/dg-guide/state_06.png) 4. Check the progress bar to see how much stETH has been added to the escrow and how much remains before the threshold is reached and the governance state changes: ![](/img/dg-guide/state_07.png) ## How to signal your opposition **Step 1: Connect your wallet** 1. Go to Dual Governance page [https://dao.lido.fi/dg](https://dao.lido.fi/dg/) 2. Click `Connect wallet` button in the upper right corner or under the proposal listing block ![](/img/dg-guide/veto_01.png) **Step 2a: Select the amount of stETH** 1. Click `Go to Veto Support` button ![](/img/dg-guide/veto_02.png) 2. Select the amount of stETH tokens you want to add to the escrow for signaling your opposition to LDO governance decisions ![](/img/dg-guide/veto_03.png) To select **wstETH**, click the second tab and select the amount to deposit. Inside the Dual Governance, deposited wstETH will be converted **to stETH at a 1:1 ratio**. ![](/img/dg-guide/veto_04.png) 3. Press `Unlock tokens and support Veto` 4. To check your tokens in Dual Governance, click the double shield icon in the upper right corner: ![](/img/dg-guide/veto_05.png) **Step 2b: Select the withdrawal NFT** If you have already requested a withdrawal [in the Lido staking widget](https://stake.lido.fi/), you can use your withdrawal NFT to support Veto Signalling. This helps delay proposal execution until your ETH has exited the protocol. 1. Click `Go to Veto Support` button 2. Select the third tab **Withdrawal NFT** 3. Select the NFT you want to use to support Veto Signalling by its ID ![](/img/dg-guide/veto_06.png) ## How to manage tokens within the Dual Governance Unless Rage Quit is triggered, you can deposit and revoke tokens from the Veto Signalling escrow at any time (subject to a minimum 5-hour timelock). Once Rage Quit is activated, all (w)stETH tokens in the escrow are automatically queued for exit. **Step 1: Switch to Manage tokens tab** ![](/img/dg-guide/veto_07.png) **Step 2: Revoke tokens from the escrow** 1. Choose the amount of tokens you want to revoke from the Veto Signalling contract ![](/img/dg-guide/veto_08.png) or select withdrawal NFT ID ![](/img/dg-guide/veto_09.png) 2. Sign the transaction **Note** that if your NFT finalizes during Veto Signaling, you must first **revoke the NFT** from the Veto Signalling escrow, then **claim your ETH** [in the Lido staking widget](https://stake.lido.fi/). ## How to withdraw tokens after the Rage Quit If Rage Quit is activated, all tokens deposited in the Veto Signalling escrow will be queued for exit. (w)stETH tokens **cannot be revoked from the escrow,** nor can the exit process be stopped **until the Rage Quit is finished** and the tokens are withdrawn. After the Rage Quit batch of tokens is finalized, they are processed as follows: - (w)stETH tokens are claimed **automatically**; - withdrawal NFTs **must be claimed manually**. Once claimed, all ETH remains in contract where LDO governance has no control over it. However, it is subject to an additional timelock of **60-180 days** before ETH becomes available for final withdrawal. The current status of your tokens and remaining timelock are indicated on the **Manage tokens** tab. ![](/img/dg-guide/rq_00.png) **Step 1: Switch to the Manage tokens tab** ![](/img/dg-guide/rq_01.png) **Step 2a: Claim your withdrawal NFT** Once your NFT is finalized, you can immediately claim it. There is **a 60-day window** after the Rage Quit is finalized to claim NFT before execution is unblocked. 1. Click `Claim` button next to the NFT ![](/img/dg-guide/rq_02.png) 2. Select the NFT by its ID ![](/img/dg-guide/rq_03.png) 3. After claiming, your ETH will be locked for 60-180 days before becoming available for withdrawal. The remaining time will be indicated on the **Manage tokens** tab ![](/img/dg-guide/rq_04.png) **Step 2b: Claim a withdrawal NFT you donโ€™t own by its ID.** If you don't currently have access to the wallet that owns the withdrawal NFT (for example, if you're managing operations from a different address), you can claim a withdrawal NFT using its ID. Note that **claiming the NFT does not transfer ownership**. It locks the corresponding ETH in the Dual Governance. **Only the NFT owner** will be able to withdraw the tokens once the lock period ends. 1. Go to the **Claim Non-Owned NFT** section ![](/img/dg-guide/rq_05.png) 2. Enter the NFT ID and click `Claim` ![](/img/dg-guide/rq_06.png) 3. The NFT owner will see the updated status in the UI when they connect their wallet ![](/img/dg-guide/rq_07.png) **Step 3: Withdraw your ETH when available** 1. Visit the **Manage tokens** tab 2. After the 60-180 days timelock expires, click the `Withdraw` button for available tokens ![](/img/dg-guide/rq_08.png) 3. Withdraw ETH. It will be deposited into your wallet. --- For more context on how Dual Governance works, when to use it, and the rationale behind timelock mechanics, check out: - [LIP 28 Dual Governance proposal](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-28.md) - [Dual Governance 101 blog article](https://blog.lido.fi/dual-governance-101-explainer/) **Need help?** If you are having issues navigating the UI, reach out [on Discord](https://discord.com/invite/lido) or [Telegram](https://t.me/lidofinance). --- # Lido Early Stakers Airdrop Claim This is how to claim Lido early stakers airdrop with Etherscan UI ## Introduction ### Who is eligible to claim Lido early stakers airdrop? LDO Airdrop could be claimed by early Lido stakers. [Here](https://github.com/lidofinance/airdrop-data/blob/main/early_stakers_airdrop.csv) you can find a list of addresses for which airdrops are available. ### How to find out the volume of available airdrop? [Here](https://github.com/lidofinance/airdrop-data/blob/main/early_stakers_airdrop.csv) you can see your airdrop LDO amount. The formula is detailed in theย [proposal](https://research.lido.fi/t/proposal-16-retroactive-airdrop-0-5-ldo-to-early-steth-users/69/18). ## Airdrop claiming ### 1. Check if you are eligible to claim airdrop Find your address [here](https://github.com/lidofinance/airdrop-data/blob/main/early_stakers_airdrop.csv) and get your index. If there is no your address [here](https://github.com/lidofinance/airdrop-data/blob/main/early_stakers_airdrop.csv) you are not eligible to claim airdrop. ### 2. Check if you havenโ€™t already claimed your airdrop 2.1 Go to [Etherscan](https://etherscan.io/address/0x4b3edb22952fb4a70140e39fb1add05a6b49622b) (contract address - [0x4b3EDb22952Fb4A70140E39FB1adD05A6B49622B](https://etherscan.io/address/0x4b3edb22952fb4a70140e39fb1add05a6b49622b)) 2.2 Paste your index on `isClaimed` method (1 row on [โ€œContract/Read contractโ€](https://etherscan.io/address/0x4b3edb22952fb4a70140e39fb1add05a6b49622b#readContract) tab) 2.3 Press the โ€œQueryโ€ button 2.4 Make sure that the method result is `false` :::note if you get `true` as a result of this step, it means that this reward was claimed earlier, and you canโ€™t claim it once again ::: ### 3. Claim your LDO airdrop 3.1 Open [โ€œContract/Write contractโ€](https://etherscan.io/address/0x4b3edb22952fb4a70140e39fb1add05a6b49622b#writeContract) tab on Etherscan 3.2 Connect your wallet to Etherscan with either MetaMask or WalletConnect 3.3 Fill-in `Claim` method fields with data from [here](https://github.com/lidofinance/airdrop-data/blob/main/early_stakers_airdrop.csv) - index (uint256) - account (address) - amount (uint256) - merkleProof (bytes32[]) 3.4 Press the โ€œWriteโ€ button and confirm the transaction in your wallet 3.5 Wait for the transaction to succeed :::note in case of invalid input transaction can be reverted ::: That's it! ๐Ÿ’ช๐ŸŽ‰๐Ÿ --- # Guide to Easy Track This guide provides information about Easy Track, voting rules, use cases, step-by-step instructions and helpful tips for initiating new motions. This guide is intended for those who use Easy Track to initiate new motions, including committee members, node operators, and others with the ability to execute their governance functions through Easy Track. The guide consists of two sections: [General overview](#general-overview) and [Operations HOWTO](#operations-howto). If youโ€™re here for the technical details of interacting with Easy Track, please skip to the latter. ## General overview ### What is Easy Track Motion? Easy Track motion is a lightweight voting process considered to have passed if the minimum objections threshold hasn't been reached. In contrast to regular Aragon voting, Easy Track motions are more cost-effective (as token holders only need to vote 'contra' if they have objections, rather than voting 'pro') and easier to manage (eliminating the need for broad DAO community voting on proposals that do not spark significant debate). To enact an Easy Track motion, the minimum objections threshold must not be reached within 72 hours after the motion has been initiated. The threshold requires support from at least 0.5% of the total LDO supply to reject the motion. To prevent motion spam, only up to 20 active motions can exist simultaneously. ### Motivation behind Easy Track Initially, the Lido DAO governance used to rely on Aragon voting model. The DAO approved or rejected proposals by direct governance token voting. Though transparent and reliable, it is not a convenient way to make decisions only affecting small groups of Lido DAO members. Besides, direct token voting didnโ€™t exactly reflect all the decision-making processes within the Lido DAO and was often used only to adopt an existing consensus. Votings on such decisions often struggled to attract wider DAO attention and thus, to pass. Easy Track has been developed as a solution to the problem of the DAO getting tired of governance. ### Easy Track use cases The main types of votes periodically initiated by the Lido DAO via Easy Track motions are listed below: - the Lido Node Operator increases its staking limit within the Lido protocol; - the Simple DVT Module Committee member manages clusters, including adding new clusters, activating or deactivating existing ones, setting cluster key limits, and updating cluster manager and reward addresses; - the Community Staking Module (CSM) Committee member updates penalties for MEV stealing; - the Lido Ecosystem Grants Organisation (LEGO) member requests fund allocations to the LEGO program; - the Lido Liquidity Observation Lab (LOL) member requests fund allocations to ongoing reward programs or adjusts the list of active reward programs; - the Rewards Share Program Committee member requests fund allocations to the program or updates the participant whitelist; - the Resourcing and Compensation Committee (RCC), Pool Maintenance Labs Ltd. (PML), or Argo Technology Consulting Ltd. (ATC) member requests grants for further allocation following their respective policies; - the TRP Multisig Committee member requests funding for TRP-related payments; - the Gas Rebates Multisig member requests funding to cover gas compensation expenses; - the Treasury Management Committee member requests tokens for swaps executed via Stonks orders. ### Possible motion outcomes A motion can have three possible outcomes: 1. **Motion passed.** In case the minimum objections threshold of 0.5% of the total LDO supply hasn't been reached, the motion is considered to have passed, and it can be enacted. This operation is permissionless, which means anyone can enact a passed motion. Please note, that it is still possible to object a non-enacted motion even after 72 hours of timelock. The enacted motion will be automatically de-activated and put to the motion archive available under the 'Archive motions' section of Easy Track UI. 2. **Motion rejected.** In case the minimum objections threshold of 0.5% of the total LDO supply has been reached, the motion is considered rejected. It will be automatically de-activated and put to the motion archive available under the 'Archive motions' section of Easy Track UI. 3. **Motion canceled.** In case you find out you have made a mistake when starting the motion, you can cancel the motion at any moment before it has been enacted. To do so, click on the motion to see the detailed motion view and press the 'Cancel' motion button top right. Please note, that this is on-chain action, and you will have to sign a transaction to complete it (gas costs apply). ### Links You can read more about Easy Track functionality in the [LIP-3](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-3.md). For more in-depth technical description, please read through the full project [specification](https://github.com/lidofinance/easy-track/blob/master/specification.md). ## Operations HOWTO - [Basic guide to Easy Track](#basic-guide-to-easy-track) - [Node Operators guide to Easy Track](#node-operators-guide-to-easy-track) - [LEGO guide to Easy Track](#lego-guide-to-easy-track) - [Liquidity Observation Lab guide to Easy Track](#liquidity-observation-lab-guide-to-easy-track) ### Basic guide to Easy Track #### Starting a new Easy Track motion To create a new Easy Track motion, follow these steps: 1. proceed to the [Easy Track UI](https://dao.lido.fi/easy-track/motions); 2. click the โ€˜Connect' button top right; 3. make sure the checkbox next to 'Terms of Use' and 'Privacy Notice' is selected; 4. select the app you want to use to connect your wallet, make sure to use an address with permission to launch motions; 6. in the header menu of Easy Track UI click the โ€˜Start motion' button (you will see the motion creation interface); 7. select the type of motion you want to launch; 8. fill out the form, make sure to complete all required fields; 9. press the โ€˜Submit' button below the form and sign the transaction (gas costs apply); 10. if a multisig was used to create the motion, another multisig owner must now confirm the transaction in Safe. As soon as the transaction is confirmed, the motion is started, and you can see it on the 'Active motions' page of Easy Track UI. Notifications will be sent to inform the DAO about the motion. From this moment on, the LDO token holders will have 72 hours to submit their objections if they have any. Please note that the motion duration may be different for testnet deployment. #### Using a multisig wallet to create a motion If you want to use a multisig to create a motion, follow these steps to connect the wallet: 1. pick the 'Wallet Connect' option; 2. copy the QR code by clicking the 'Copy to clipboard' button under the code; 3. proceed to the [Safe](https://app.safe.global/), connect your wallet by clicking 'Connect your wallet' button top right; 4. open the 'Apps' section in the drawer menu on the left and find the Wallet Connect Safe app in the list; 5. paste the code into the field on the left. Now the multisig is connected to the Easy Track app. You must keep the Wallet Connect Safe app tab open in your browser for transactions to pop up. You will not receive transaction requests if you don't have it open. #### Checking the motion details from Safe Multisig UI When a motion start transaction is created by one of the multisig signers, the remaining signers should review the addresses and transaction data before signing. To verify the transaction, follow these steps: 1. Check the address the tx is being sent to โ€” it should be `Easy Track` contract listed on the [Deployed Contracts page](/deployed-contracts/#easy-track). 2. Check the params of the `createMotion` call: 1. to check `_evmScriptFactory` address use [Deployed Contracts page](/deployed-contracts/#easy-track) โ€” the address should be listed on this page & match the type of motion is about to be started. 2. to check `_evmScriptCallData` bytes string open the `_evmScriptFactory` contract on the etherscan & call the `decodeEVMScriptCallData` with the string from the Safe UI to see the motion params. ### Node Operators guide to Easy Track **Motion type:** - To initiate a new motion, select **Increase Node Operator Staking Limit** as the motion type. **How to complete the form for a new motion:** - Use your Node Operator ID, which can be found in the Node Operators Dashboard on [holeลกky](https://operators-holesky.testnet.fi/) or [mainnet](https://operators.lido.fi/) (it is the number displayed to the right of your node operator name with the # prefix). - Enter the desired staking limit in the โ€˜New Limitโ€™ field. **Other key considerations:** 1. **Node operators can only increase staking limits for themselves.** Before initiating a motion, ensure that you have access to the address associated with the correct node operator in the Lido Node Operators Registry. You can find the correct address in the Node Operators Dashboard on [holeลกky](https://operators-holesky.testnet.fi/) or [mainnet](https://operators.lido.fi/)). 2. **A single motion can only address the staking limit of a single node operator.** It is not possible to increase limits for multiple node operators in one motion. 3. **The total amount of a node operator's signing keys must be greater than or equal to the new staking limit.** Make sure you have submitted enough valid signing keys before starting a motion. ### LEGO guide to Easy Track **Motion type:** - To initiate a new motion, select **Top up LEGO** as the motion type. **How to complete the form for a new motion:** - Pick the token and specify the amount of tokens you want to top up the LEGO program with. - You can add multiple token allocations into a single motion by clicking 'One more token' below the form. **Other key considerations:** 1. **Only a LEGO committee member can start a motion to allocate funds to LEGO program.** Before starting a motion, please make sure you have access to [the LEGO Committee multisig](https://app.safe.global/settings/setup?safe=eth:0x12a43b049A7D330cB8aEAB5113032D18AE9a9030). 2. **LEGO Easy Track motions support fund allocation in one or multiple of the following tokens: DAI, USDC, USDT and LDO.** ### Liquidity Observation Lab guide to Easy Track **Motion type:** - To initiate a new motion, select **Add stETH reward program**, **Remove stETH reward program** or **Top up stETH reward program** as the motion type. **How to complete the form for a new motion:** - When adding a new program, the title should be a human-readable description of the reward program (e.g. 'Curve ETH:stETH LP incentives'). - Fill the Ethereum address of the reward program (it could be reward contract or reward manager contract depending on the specific program) in the 'Address' field. - When creating a motion to remove a reward program from the list or to top up a previously added program, you will be able to pick a program by the program title, rather than pasting Ethereum address. UI for topping up the rewards program takes full tokens as an input (so, the amounts are in X LDOs, not X*1e18 LDO Weis). **Other key considerations:** 1. **Only a Lido Liquidity Observation Lab member can start a motion to allocate funds to reward programs.** Before starting a motion, please make sure you have access to [the Liquidity Observation Lab multisig](https://app.safe.global/settings/setup?safe=eth:0x87D93d9B2C672bf9c9642d853a8682546a5012B5). 2. **Liquidity Observation Lab Easy Track motions support fund allocation in stETH only.** 3. **Easy Track supports topping up multiple reward programs in a single motion.** Though be careful, lack of consensus on one reward program will prevent the whole motion from passing. 4. **To top up a reward program via Easy Track motion, it should first be added into the list of active reward programs.** This action requires a separate Easy Track motion to complete. 5. **When no longer active, reward program should be removed from the list of active reward programs.** This action requires a separate Easy Track motion to complete. --- # EDF Operator Guide โ€” Lido Oracle & Council Daemon Setup instructions for operators (key holders) of a Lido Oracle seat or a DSM guardian seat moving to the **Execution Delegation Framework (EDF)**. **Reference material:** - [LIP-37: Execution Delegation Framework](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md) โ€” the proposal - [execution-delegation-framework](https://github.com/lidofinance/execution-delegation-framework) โ€” the contracts, [architecture](https://github.com/lidofinance/execution-delegation-framework/blob/main/docs/architecture.md), [usage guide](https://github.com/lidofinance/execution-delegation-framework/blob/main/docs/usage.md) - [EDF Operator Key Custody Policy](./key-custody-policy-for-edf-operators.md) โ€” the rules you must follow - [EDF Rotation and Incidents](./edf-rotation-and-incidents.md) โ€” what to do after the setup --- ## What changes The protocol permission moves from your hot key to a contract you own โ€” your `DelegationContract`. Your hot key becomes its **delegate**, and you can rotate or revoke it yourself, with no vote. | | Before EDF | After EDF | | --- | --- | --- | | Who holds the Oracle / guardian seat | your hot EOA | your `DelegationContract` | | Who signs and pays gas | your hot EOA | your delegate EOA (still a hot key) | | Rotating the hot key | governance vote, ~10 days | `nominateDelegate()`, effective after 48 h | | Killing a stolen hot key | governance vote | `revokeDelegate()`, effective immediately | | Who can call `nominateDelegate` / `revokeDelegate` | โ€” | **multisig participants** | Two key classes: - **Owner key** โ€” a **Safe multisig**, 2-of-3 or stronger, with hardware cold wallets recommended for the signers. Called **the multisig** everywhere below. - **Delegate key** โ€” the hot key on the daemon host. --- ## Part 0 โ€” Set up the owner multisig The owner address and the cooldown are fixed at deployment and **cannot be changed on-chain**. Redoing them means a new `DelegationContract` and a governance vote. ### 0.1. Read the custody policy Read the [Key Custody Policy](./key-custody-policy-for-edf-operators.md) before you generate anything. Two of its values are irreversible: - the **owner address** โ€” your multisig (step 0.2); - the **cooldown** โ€” **48 hours = `172800` seconds**. ### 0.2. Prepare the owner multisig Create the multisig that will own your `DelegationContract`: | Network | Where to create the multisig | | --- | --- | | Ethereum mainnet | [app.safe.global](https://app.safe.global/welcome/spaces) | | Hoodi | [app.safe.protofire.io](https://app.safe.protofire.io/welcome) โ€” the official Safe UI does not support Hoodi | Pick the network first โ€” one multisig per network: ![Safe creation wizard step 1 on app.safe.global: name and Select Networks with Ethereum chosen](./screenshots/safe-mainnet-network.jpg) On Hoodi the wizard is the same, at [app.safe.protofire.io](https://app.safe.protofire.io/welcome): ![Safe creation wizard step 1 on Protofire: Select Networks with Hoodi Testnet chosen](./screenshots/safe-hoodi-network.jpg) Requirements: - **at least 3 signers, threshold at least 2** (2-of-3 or stronger); - this multisig is used for **nothing else** โ€” no treasury, no other roles. ![Safe creation wizard step 2: three signer addresses and a threshold of 2 out of 3 signers](./screenshots/safe-mainnet-signers-threshold.jpg) **Recommended:** every signer a **hardware cold wallet** (Ledger, Trezor). A software wallet is acceptable. Write down who the signers are and how to reach them out of hours. --- ## Part 1 โ€” Set up your seat ### 1.1. Generate the delegate hot key Generate a fresh key. Do not reuse the EOA that holds your seat today. ### 1.2. Deploy your `DelegationContract` from the factory The `DelegationFactory` is already deployed by the Lido contributors โ€” you only call it. | Network | `DelegationFactory` address | | --- | --- | | Ethereum mainnet | [`0xD990770eB2B4b6062EDdB06892fF179C693b46e6`](https://etherscan.io/address/0xD990770eB2B4b6062EDdB06892fF179C693b46e6#code) | | Hoodi | [`0xEb49f72DB1546B0E63e1114E2e403edbcE722AE6`](https://hoodi.etherscan.io/address/0xEb49f72DB1546B0E63e1114E2e403edbcE722AE6#code) | **Do not accept a factory address from chat or DM.** Take it from the table above or from the [Deployed Instances table](https://github.com/lidofinance/execution-delegation-framework#deployed-instances), and check that Etherscan shows it verified under the name `DelegationFactory`. The call: ``` deploy(address owner, address delegate, uint256 cooldown) ``` Copy the multisig address from its dashboard: ![Safe dashboard with the account address and a Copy address button](./screenshots/safe-address-copy.jpg) | Argument | Value | | --- | --- | | `owner` | your multisig address from its dashboard (see screenshot) | | `delegate` | your delegate EOA from step 1.1 | | `cooldown` | `172800` (48 hours) | `owner` and `delegate` must be different addresses, and `delegate` must not be `address(0)`. **In Etherscan** (or Blockscout, Otterscan): 1. Open the factory address โ†’ **Contract** โ†’ **Write Contract**. 2. **Connect to Web3** with your owner multisig through WalletConnect โ€” the same flow as in the [Safe + Etherscan example](./edf-rotation-and-incidents.md). Deploying from the multisig also verifies that you control the owner address. 3. Expand `deploy`, fill in the three values, send the transaction. ![Etherscan Write Contract tab with the deploy function expanded, showing the owner, delegate and cooldown fields](./screenshots/etherscan-deploy-form.jpg) 4. Open the transaction โ†’ **Logs** tab โ†’ `DelegationContractDeployed(instance, owner, delegate, cooldown)`. Save the **`instance`** address: that is your `DelegationContract`. ![Etherscan Logs tab showing InitialDelegateSet and DelegationContractDeployed with instance, owner, delegate and cooldown 172800](./screenshots/etherscan-deploy-logs-owned.jpg) ### 1.3. Verify what you deployed Open your `DelegationContract` on Etherscan โ†’ **Contract** โ†’ **Read Contract**: | Method | Expected | | --- | --- | | `owner()` | your multisig address | | `getDelegate()` | your delegate EOA | | `getPendingDelegate()` | `0x0000โ€ฆ0000`, `0` | | `getCooldown()` | `172800` | | `isTerminated()` | `false` | Press **Expand All** to see every value at once: ![Etherscan Read Contract tab with all methods expanded, showing getCooldown 172800, getDelegate and isTerminated False](./screenshots/etherscan-read-contract.jpg) Open the deploy transaction's **Logs** tab and confirm `InitialDelegateSet(newDelegate)` carries the delegate address you intended. From a terminal: ```bash cast call "owner()(address)" --rpc-url $RPC_URL # your multisig cast call "getDelegate()(address)" --rpc-url $RPC_URL # your delegate EOA cast call "getPendingDelegate()(address,uint256)" --rpc-url $RPC_URL # 0x0โ€ฆ0, 0 cast call "getCooldown()(uint256)" --rpc-url $RPC_URL # 172800 cast call "isTerminated()(bool)" --rpc-url $RPC_URL # false ``` If anything does not match, the deploy parameters were wrong โ€” deploy another contract from the factory. ### 1.4. Set up your own monitoring and alerts Lido runs protocol-wide monitoring; monitor your own contract independently. **Should page a human 24/7** โ€” events on your `DelegationContract`: - `DelegateNominated(newDelegate, activeFrom)` โ€” if you did not do it, your multisig is compromised. React before `activeFrom` (48 h). - `DelegateRevoked(revokedDelegate)` - `Terminated()` Route these to a phone. **Should alert** โ€” unusual delegate activity: - `execute()` calls to targets your daemon never calls, or to an EOA; - non-zero `msg.value` forwarded through `execute()`; - direct transactions from the delegate EOA that your daemon did not send. ### 1.5. Publish your addresses For a testnet seat, the internal operators' Telegram chat is enough. The forum post is for mainnet. Post in the LIP-37 thread on the Lido research forum: **โ†’ [LIP-37: Execution Delegation Framework (EDF)](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/6)** Copy the block below, fill in the addresses, and keep only the lines that are true. If a line is not true yet, finish that step first. ```markdown **Seat:** Lido Oracle / DSM guardian **DelegationContract:** **Owner multisig:** **Delegate EOA:** - [x] I have read the EDF Operator Key Custody Policy and my setup follows it - [x] I created a dedicated owner multisig, at least 2-of-3, with signers held by different people on different devices - [x] The multisig is used for nothing except this delegation contract - [x] I deployed my DelegationContract from the official DelegationFactory, with a 48-hour (172800 s) cooldown - [x] I verified on-chain that owner(), getDelegate(), getCooldown() and isTerminated() are what I intended - [x] I set up 24/7 alerts on DelegateNominated, DelegateRevoked and Terminated ``` --- ## Part 2 โ€” Configure the Lido Oracle > Follow **Part 2** if you run the Lido Oracle, **Part 3** if you run the Council daemon. ### 2.1. Configure the oracle 1. **Set the environment variables.** | Variable | Value | | --- | --- | | `DELEGATION_CONTRACT_ADDRESS` | Your `DelegationContract` address. When empty, delegation is off. | | `MEMBER_PRIV_KEY` / `MEMBER_PRIV_KEY_FILE` | **The old key** โ€” your existing member EOA. | | `MEMBER_PRIV_KEY_2` / `MEMBER_PRIV_KEY_2_FILE` | **The new key** โ€” the delegate of your `DelegationContract`. | ```bash DELEGATION_CONTRACT_ADDRESS=0xYourDelegationContract MEMBER_PRIV_KEY=0xoldmemberkey # old - active until the vote MEMBER_PRIV_KEY_2=0xnewdelegatekey # new - takes over after the vote ``` 2. **Fund the delegate EOA.** Send 50% of the current balance of your old member EOA to the new delegate EOA (the address returned by `getDelegate()`). Both keys must be able to pay for gas: the old one until the vote, the new one after it. 3. **Restart the oracle.** ### 2.2. Check that the oracle works โ€” in the logs At startup: - `Initialize delegation contract.` with your address โ€” config is read correctly. - `Delegation contract is a member, but its current delegate matches none of the configured accounts.` โ€” fix the config. - `None of the configured accounts is an active member.` โ€” fix the config. - `Provided Account is not part of Oracle's members and has no submit role.` โ€” fix the config. ### 2.3. Report your oracle setup in the operators' chat After the oracle setup is ready, write an announcement in the holders' Telegram chat, before the governance vote: ```markdown Oracle daemon ready for EDF โ€” DelegationContract - [x] DELEGATION_CONTRACT_ADDRESS is set to my DelegationContract - [x] Both keys are configured: MEMBER_PRIV_KEY (old member EOA) and MEMBER_PRIV_KEY_2 (new delegate) - [x] The new delegate EOA is funded - [x] I restarted the oracle and saw no configuration errors in the logs ``` ### 2.4. After the governance vote #### Check the delegated path on Etherscan Reports now arrive as **internal transactions**: the delegate calls `execute()` on your `DelegationContract`, which calls the oracle contract. Check three address pages: | Open | Tab | What you must see | | --- | --- | --- | | your `DelegationContract` | **Internal Transactions** | outgoing calls to the oracle contracts, starting at the moment the delegate became effective | | your **old** member EOA | **Transactions** | its calls to the oracle contracts **stopped** at that same moment | | your **new** delegate EOA | **Transactions** | calls to your `DelegationContract` and **nothing else** | If the delegate EOA is calling an oracle contract **directly**, `DELEGATION_CONTRACT_ADDRESS` is unset or wrong โ€” fix the config. #### Retire the old key > **โš  Do this only once the new delegate has produced a successful report** and the checks above > pass. Not when the vote passes, and not when `getDelegate()` returns the new address. 1. Move the delegate key into `MEMBER_PRIV_KEY` and clear `MEMBER_PRIV_KEY_2`. 2. Restart the oracle. 3. Delete the old key from your secrets store. 4. Move the old address's remaining balance to the new delegate address. --- ## Part 3 โ€” Configure the Council daemon (DSM guardian) ### 3.1. Configure the council daemon 1. **Set the environment variables.** | Variable | Value | | --- | --- | | `DELEGATION_CONTRACT_ADDRESS` | Your `DelegationContract` address. Config validation **fails at startup** if it is empty or not a valid address โ€” even while the DSM is still on v4. | | `WALLET_PRIVATE_KEY` / `WALLET_PRIVATE_KEY_FILE` | **The old key** โ€” your existing guardian EOA. Used while the DSM is on v4. | | `WALLET_PRIVATE_KEY_2` / `WALLET_PRIVATE_KEY_2_FILE` | **The new key** โ€” the delegate of your `DelegationContract`. | ```bash DELEGATION_CONTRACT_ADDRESS=0xYourDelegationContract WALLET_PRIVATE_KEY=0xoldguardiankey # old - active until DSM v5 WALLET_PRIVATE_KEY_2=0xnewdelegatekey # new - takes over at DSM v5 ``` 2. **Fund the delegate EOA.** Send 50% of the current balance of your old guardian EOA to the new delegate EOA (the address returned by `getDelegate()`). Both keys must be able to pay for gas: the old one until DSM v5, the new one after it. Do the same on the DataBus chain (Gnosis): the delegate EOA needs xDAI there to send Data Bus messages. 3. **Restart the daemon.** ### 3.2. Check that the daemon works โ€” in the logs The daemon reports its mode on every processed block: ``` Guardian execution mode: edf delegateAddress: 0x... โ† the hot key actually in use guardianAddress: 0x... โ† your DelegationContract dsmAddress: 0x... dsmVersion: 5 ``` Errors you may hit, and what they mean: | Error | Meaning | | --- | --- | | `DELEGATION_CONTRACT_ADDRESS is required for DSM version 5` | Variable not set. | | `No contract code at DELEGATION_CONTRACT_ADDRESS 0xโ€ฆ` | Wrong address, or wrong network. | | `DelegationContract 0xโ€ฆ is terminated` | Someone called `terminate()`. The seat is permanently dead. | | `DelegationContract 0xโ€ฆ has no active delegate` | The delegate was revoked, or never set. Expected right after an emergency revocation. | | `DelegationContract 0xโ€ฆ does not support ERC-1271` | The address is not an EDF delegation contract. | | `No configured wallet private key matches active delegate 0xโ€ฆ` | The on-chain delegate is neither `WALLET_PRIVATE_KEY` nor `WALLET_PRIVATE_KEY_2`. Add the key and restart. | | `An error occurred when sending a message using Data Bus` with `UNPREDICTABLE_GAS_LIMIT` | The delegate EOA has no xDAI on the DataBus chain, so `estimateGas` fails with "gas required exceeds allowance (0)". Fund the delegate EOA on Gnosis (step 3.1.2). The daemon also warns with `DataBusService account balance is too low`. | ### 3.3. Report your council setup in the operators' chat After the council daemon setup is ready, write an announcement in the holders' Telegram chat, before the governance vote: ```markdown Council daemon ready for EDF โ€” DelegationContract - [x] DELEGATION_CONTRACT_ADDRESS is set to my DelegationContract - [x] Both keys are configured: WALLET_PRIVATE_KEY (old guardian EOA) and WALLET_PRIVATE_KEY_2 (new delegate) - [x] The new delegate EOA is funded on Ethereum and on Gnosis - [x] I restarted the daemon and saw no configuration errors in the logs ``` ### 3.4. After the governance vote #### Check the delegated path on Etherscan A guardian writes on-chain rarely โ€” only `pauseDeposits` and `unvetSigningKeys` produce transactions, and they now arrive as **internal transactions** through your `DelegationContract`. Check three address pages: | Open | Tab | What you must see | | --- | --- | --- | | your `DelegationContract` | **Internal Transactions** | calls to the DSM โ€” empty until the first pause or unvet, which is normal | | your **old** guardian EOA | **Transactions** | its calls to the DSM **stopped** at the cutover | | your **new** delegate EOA | **Transactions** | calls to your `DelegationContract`, plus messages to the DataBus contract on the DataBus chain โ€” never a direct call to the DSM | A transaction sent **directly** from the delegate EOA to the DSM means the daemon is still in `legacy-eoa` mode, or the delegate key was configured as a plain guardian somewhere. #### Retire the old key > **โš  Do this only once the daemon is confirmed running in `edf` mode** โ€” the log reading > `Guardian execution mode: edf` with `dsmVersion: 5`, and pings and messages still flowing. 1. Move the delegate key into `WALLET_PRIVATE_KEY` and clear `WALLET_PRIVATE_KEY_2`. 2. Restart the daemon. 3. Delete the old key from your secrets store. 4. Move the old address's remaining balance to the new delegate address - on Ethereum and on the DataBus chain (Gnosis), if you have not done it in step 3.1.2 yet. --- Setup is done. Routine key rotation and emergency procedures are in **[EDF Rotation and Incidents](./edf-rotation-and-incidents.md)**. --- # EDF Rotation and Incidents Delegate key rotation and emergency procedures for an EDF seat. - [EDF Operator Guide](./edf-operator-guide.md) โ€” the setup - [EDF Operator Key Custody Policy](./key-custody-policy-for-edf-operators.md) โ€” the rules you must follow
Example: running an Etherscan Hoodi transaction from a Safe wallet `nominateDelegate`, `revokeDelegate` and `terminate` are `onlyOwner` โ€” the caller must be the multisig. A plain MetaMask connection sends them from your own EOA and they revert with `NotOwner`. 1. Open your `DelegationContract` on Etherscan โ†’ **Contract** โ†’ **Write Contract** โ†’ **Connect Wallet** โ†’ **WalletConnect** โ†’ **All Wallets**. A QR code appears โ€” copy the pairing link (`wc:...`) next to it. Do **not** pick MetaMask. 2. In the Safe UI, click the **WalletConnect** icon in the header, paste the link into **Pairing code**, and approve the session. The link expires within minutes โ€” paste it right after copying. ![Safe WalletConnect panel with the Pairing code field and Etherscan connected](./screenshots/safe-walletconnect-panel.jpg) 3. Etherscan's header must now show the **multisig address**, not your EOA. 4. Fill in the method and press **Write**. ![Etherscan Write Contract on a DelegationContract: execute, nominateDelegate, revokeDelegate and terminate](./screenshots/etherscan-delegation-methods.jpg) 5. The call lands in the multisig queue. Signers confirm it with their own wallets, then anyone executes it and pays the gas. ![Safe Confirm transaction screen showing the call from Etherscan to the DelegationContract](./screenshots/safe-confirm-nominate.jpg)
--- ## Routine rotation Rotate at least **once a year**; quarterly is recommended. Also rotate when an engineer with host or secrets access leaves, when the host is rebuilt from an untrusted image, or when the key's history is unknown. 1. **Generate** the new key on the target host (step 1.1 of the guide applies). 2. **Announce** at least **1 day** ahead on the research forum and in the operators' channel. Oracle operators: also send the new delegate address to node operators for their `ORACLE_ADDRESSES_ALLOWLIST`. 3. **Stage it in the daemon**, keeping the current key in place: - **Oracle:** set `MEMBER_PRIV_KEY_2` to the new key. Restart once. - **Council:** set `WALLET_PRIVATE_KEY_2` to the new key, keeping `WALLET_PRIVATE_KEY` as it is. Restart once. 4. **Nominate** from the owner multisig, a day after the announcement: ``` nominateDelegate() ``` 5. **Verify your own nomination.** Read `getPendingDelegate()` on Etherscan, or: ```bash cast call "getPendingDelegate()(address,uint256)" --rpc-url $RPC_URL ``` The address and `activeFrom` must be exactly what you intended. 6. **Fund the new address** โ€” send it half of the current delegate's balance. 7. **At `activeFrom`** the switch happens with no transaction and no restart. Verify: ```bash cast call "getDelegate()(address)" --rpc-url $RPC_URL # == new delegate ``` - Oracle: confirm a successful report in the following frame. - Council: the log shows the new `delegateAddress`; confirm pings and messages continue. 8. **Only after that succeeds, retire the old key:** - **Oracle:** move the new key into `MEMBER_PRIV_KEY` and clear `MEMBER_PRIV_KEY_2`. Restart. - **Council:** move the new key into `WALLET_PRIVATE_KEY` and clear `WALLET_PRIVATE_KEY_2`. Restart. - Delete the old key from your secrets store. - Move the old address's remaining balance to the new delegate address. Notes: - Calling `nominateDelegate` again during the cooldown **replaces** the pending delegate and **restarts** the 48 hours. - It reverts if the address is zero, equals the owner, equals the current delegate, or equals the pending delegate. - Never stage a second key on a host you suspect is compromised. ## Emergency: the delegate hot key may be compromised Triggers: signatures or transactions you did not originate, host intrusion indicators, a secrets store breach, malware on the host, or accidental disclosure (pasted in chat, committed to a repo, captured in logs). **Revoke first, investigate second.** 1. From the owner multisig, call: ``` revokeDelegate() ``` It takes effect immediately and cancels any rotation in flight. 2. **Notify** the holders' Telegram chat as soon as the transaction is sent: seat, revoked key, known facts, and as much evidence as you can collect. 3. **Re-key on clean infrastructure**: new key on a rebuilt or verified host, funded, added to the daemon config, then `nominateDelegate(newKey)` from the multisig. The seat comes back **48 hours later**. 4. **Publish a post-incident report** (timeline, root cause, exposure window, custody changes) on the forum, or in the holders' Telegram chat if disclosure is sensitive. While revoked, the Council daemon logs `DelegationContract 0xโ€ฆ has no active delegate` every block and the Oracle logs a warning each cycle. This stops once the new delegate activates. ## Emergency: the owner multisig may be compromised Triggers: unexpected changes to the multisig participants, unexpected multisig activity, or a compromised signer device with any doubt about the rest of the quorum. 1. If the owner itself can no longer be trusted, call from the multisig: ``` terminate() ``` **This is irreversible.** It disables `execute()`, fails all signature verification closed, and clears the delegate forever. 2. **Notify governance and the holders' Telegram chat immediately.** Restoring the seat needs a *new* `DelegationContract` with a *new* owner multisig **and a governance vote**. You have exactly one cooldown (48 h) between a hostile `DelegateNominated` and it becoming effective. --- # EDF Operator Key Custody Policy > ๐Ÿ” Policy for [LIP-37: Execution Delegation Framework](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-37.md). Applies to operators of permissioned roles behind an EDF `DelegationContract`, initially Lido Oracle committee members and DSM guardians. **Version:** 1.0 **Applies to:** Operators of permissioned roles behind an EDF `DelegationContract` **Maintained:** On the Lido research forum; may be revised without a protocol change The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, and **MAY**, when they appear in uppercase, are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). --- ## 1. Purpose and scope LIP-37 moves key rotation from a ~10-day governance vote to a local operator action. That only improves security if key holders store, rotate, and revoke keys with discipline. This document defines that discipline. It covers: - The two key classes in the EDF model - Custody requirements for each key class - Rotation cadence - Required response to suspected or confirmed compromise --- ## 2. Key classes | Key | Role in EDF | Exposure | Custody class | | --- | --- | --- | --- | | **Owner key** | Controls the `DelegationContract`: `nominateDelegate()`, `revokeDelegate()`, `terminate()` | Used rarely for rotations and incidents | **Cold** โ€” Safe multisig; hardware cold-wallet signers RECOMMENDED | | **Delegate key** | Hot signing key stored in and used by the off-chain daemon | Online continuously; assumed compromisable | **Hot** โ€” machine-resident, minimized blast radius, rotated routinely | The two classes have opposite design goals: - The **owner key** is the security boundary of the whole model. It must be nearly impossible to steal, even at the cost of being slow to use. - The **delegate key** is expected to be exposed by its nature. The policy goal is not to make it unstealable, but to keep it worthless quickly through narrow permissions, short lifetime, and instant revocation. --- ## 3. Owner key custody The owner address is fixed at deployment and cannot be changed on-chain. Replacing it means deploying a new `DelegationContract` and passing a governance vote to reassign the seat. Treat the owner setup as a long-lived commitment and get it right before deployment. 1. **The owner MUST be a Safe multisig.** A bare EOA owner is not acceptable for Oracle or DSM seats. 2. **The owner MUST be dedicated to its assigned activity.** The multisig MUST be responsible only for the single activity it was assigned to perform: key delegation management. It MUST NOT be used for any other purpose. 3. **The multisig MUST be at least 2-of-3.** A higher threshold or more signers is acceptable. 4. **Every signer SHOULD be a hardware cold wallet.** Hardware cold wallets, such as Ledger or Trezor, are RECOMMENDED for all signers. 5. **Multisig signer set changes MUST be executed promptly.** Every signer set change SHOULD be paired with a hot key rotation. - **Departure or role change.** No later than the personโ€™s last day of access. Removing the signer MUST NOT delay revocation of their other access. - **Lost or stolen signer device, or exposed seed backup.** Within **24 hours** of the loss being reported. If the affected signer together with any other doubtful signer would meet the threshold, treat it as a ยง6.2 event. - **Suspected compromise of the signerโ€™s computer, or coercion.** Within **24 hours**. - **Routine device replacement, or a signer who cannot be reached out of hours.** Within **5 business days**. --- ## 4. Delegate hot-key custody 1. **Use one key per seat and environment.** A delegate key MUST be unique to a single `DelegationContract` and a single environment. It MUST NOT be reused across mainnet/testnet, across Oracle and Council daemons, or for anything besides its seatโ€™s duties. 2. **Harden the host.** The daemon host SHOULD be dedicated to the role, with: - Access limited to named engineers - Audited access channels - No shared SSH accounts - Current OS and daemon versions - No unrelated internet-facing services 3. **Keep only minimal balance.** The delegate address MUST hold only working gas funds. A low-balance alert SHOULD be configured. 4. **Delegate keys MUST be dedicated to their assigned activity.** Each hot key MUST be responsible only for the single activity it was assigned to perform (day-to-day protocol operation). It MUST NOT be used for any other purpose. --- ## 5. Rotation policy EDF makes rotation seamless: `nominateDelegate(newKey)` keeps the old key effective until the new one activates after the cooldown. This section applies to delegate rotations after the EDF migration is complete. During the initial migration, the existing hot EOA is configured as the initial delegate and is effective immediately; the governance action reassigning the seat from that EOA to its `DelegationContract` is the migration cutover. A `DelegationContract` authorizes exactly one effective delegate at a time. Before `activeFrom`, the current delegate remains effective. Starting at `activeFrom`, the nominated delegate becomes effective automatically and the previous EOA loses its authority through that `DelegationContract`, although the EOA itself continues to exist. 1. **Routine cadence** The delegate key MUST be rotated at least every **1 year**. Quarterly rotation is RECOMMENDED. 2. **Event-driven rotation** Independent of cadence, the delegate key MUST be rotated when: - An engineer with access to the daemon host or secrets store leaves the organization or changes role - The daemon host is migrated or rebuilt from an untrusted image - Any dependency or infrastructure incident could have exposed the key - The keyโ€™s age or custody history is unknown If exposure is suspected rather than merely possible, this becomes revocation, not rotation. 3. **Announce rotations** Routine rotations MUST be announced on the Lido research forum at least **1 day** before `nominateDelegate()` is executed and in the operatorsโ€™ coordination channel before execution. This lets monitoring parties distinguish a planned `DelegateNominated` from a hostile one. 4. **Planned rotation procedure** 1. Generate the new key. 2. Publish the pre-nomination announcement on the research forum. 3. Add the replacement key to the daemon as its staged secondary member key. Keep the current delegate configured and operating. 4. On behalf of the owner, execute `nominateDelegate(newKey)` on your `DelegationContract`. The old key remains effective during the cooldown. 5. Watch for your own `DelegateNominated` event and verify that the delegate and `activeFrom` returned by `getPendingDelegate()` match the intended rotation. 6. Fund the replacement address from the current delegate address with half of its balance. 7. During the cooldown, the daemon MUST continue using the current delegate. 8. After activation: - Verify `getDelegate() == newKey`. - Confirm that the daemon selected the new key. - Confirm a successful report or message in the following applicable frame. 9. Only after successful verification: - Remove the previous key from the daemon configuration and secrets store. - Move the previous EOAโ€™s remaining balance to the new delegate address. 5. **Owner rotation** Multisig signer keys follow ยง3.5. Replacing the multisig itself requires a new `DelegationContract` deployment and a governance vote. --- ## 6. Incident response Speed is the point of EDF. The contract lets operators drop a key in one transaction; this section defines when they must. ### 6.1 Suspected or confirmed delegate hot-key compromise Triggers include: - Signatures or transactions you did not originate - Host intrusion indicators - Secrets-store breach - Malware on the daemon host - Accidental key disclosure, such as pasting in chat, committing to a repo, or capturing in logs Response: 1. **Revoke first, investigate second.** The owner MUST call `revokeDelegate()` immediately upon suspicion. Revocation takes effect immediately: it clears both the current delegate and any pending one, and signature verification through the contract fails closed from that moment on. If a rotation is in flight, revocation cancels it โ€” the staged replacement must be nominated again once the seat is safe to restore. 2. **Notify security and operators.** Notify the holdersโ€™ Telegram chat as soon as the revocation transaction is sent. Include: - Seat - Revoked key - Known facts - As much evidence as you can collect 3. **Re-key on clean infrastructure.** Generate a replacement per ยง4 on a host you trust, rebuilt or verified clean, and call `nominateDelegate(newKey)`. The seat resumes after the cooldown. 4. **Publish a post-incident report.** Publish a summary to the research forum, or to the holdersโ€™ Telegram chat if disclosure is sensitive. Include: - Timeline - Root cause - Exposure window - Custody changes made ### 6.2 Suspected owner cold-key / multisig compromise Triggers include: - Unexpected changes to the multisig participants - Unexpected multisig activity - A compromised signer device combined with any doubt about the rest of the quorum Response: 1. **If the owner itself can no longer be trusted:** The owner MUST call `terminate()`. Termination is irreversible. It disables `execute()`, fails all signature verification closed, and clears the delegate. A dead seat is strictly better than a stolen one. 2. **Notify immediately.** Notify the holdersโ€™ Telegram chat immediately. Governance will need to reassign the seat to a freshly deployed `DelegationContract` with a new owner multisig, so early notice shortens downtime. --- ## 7. Monitoring Alongside Lidoโ€™s protocol-wide monitoring, each operator SHOULD independently monitor their own contract. ### Recommended alerts - **`DelegateNominated`, `DelegateRevoked`, and `Terminated` events** on the operatorโ€™s `DelegationContract` - SHOULD alert a human 24/7 - An unexpected `DelegateNominated` is the primary owner-compromise signal - The owner MUST react to an unexpected nomination before the cooldown elapses - **Delegate address activity** outside the daemonโ€™s expected pattern - Unexpected `execute()` targets, including EOA destinations - Unexpected non-zero `msg.value` forwarded through `execute()` - Transactions from the delegate EOA itself ### Emergency contact Each operator MUST provide a fast contact channel for emergencies, where a human can be reached at any time, and MUST keep it current. --- # How to Vote, Override, Delegate with Etherscan This guide will walk you through how to vote, override your delegate's vote, and delegate your voting power using Etherscan. If the [Voting UI](https://dao.lido.fi/vote/dashboard) is unavailable or you prefer to vote via Etherscan, follow these simple steps. ## Getting Started Obtain the address of the Lido DAO `Aragon Voting` contract from the [Deployed Contracts](/deployed-contracts/#dao-contracts) page. Currently, it is [`0x2e59A20f205bB85a89C53f1936454680651E618e`](https://etherscan.io/address/0x2e59A20f205bB85a89C53f1936454680651E618e). Open this contract on Etherscan. ## Voting ### Step 1: Find the Voting ID - Go to the **Contract** tab. ![](/img/etherscan-voting/Contract_6.png) - Go to the **Read as Proxy** tab of the Aragon Voting contract. - Locate the `votesLength` method (number 29) to get the current vote ID. ![](/img/etherscan-voting/vote_ID_1.png) The number you see here is the ID of the current vote. For example, if it shows 110, that's the current vote ID. ### Step 2: Review the Proposal - Use the `getVote` method (number 9) to review the proposal. Note that to understand the proposed changes, you will need to decode the bytecode into readable scripts. ![](/img/etherscan-voting/getVote_2.png) ### Step 3: Cast Your Vote - Navigate to the **Write as Proxy** tab. - Click **Connect to Web3** and connect the address where you hold LDO tokens. The indicator should turn green. ![](/img/etherscan-voting/web3_connect_3.png) - Use the `vote` method (number 13). ![](/img/etherscan-voting/vote_4.png) - Fill in the parameters `_voteId`, `_supports`, and `_executesIfDecided` and send the transaction: - `_voteId` is the vote ID from Step 1. - `_supports` indicates whether you support (`true`) or oppose (`false`) the vote. - `_executesIfDecided` should be set to `false`. ### Step 4: Sign the Transaction Sign the transaction to cast your vote. That's it! ๐ŸŽ‰ ## Overriding ### Step 1: Check Delegate's Vote - Go to the **Contract** tab. ![](/img/etherscan-voting/Contract_6.png) - Go to the **Read as Proxy** tab of the Aragon Voting contract. - Use the `getVoterState` method (number 7). ![](/img/etherscan-voting/getVoterState_5.png) Enter the vote ID and your address to see how your delegate voted. ### Step 2: Vote Yourself If you disagree with the delegate's choice and wish to vote yourself, follow the steps in the **Voting Steps** section. ## Delegating Through Etherscan ### Assign a Delegate 1. Go to the **Contract** tab. ![](/img/etherscan-voting/Contract_6.png) 2. Go to the **Write as Proxy** tab. 3. Use the `assignDelegate` method (number 1). 4. Click **Connect to Web3** and connect your address. The indicator should turn green. 5. Enter your delegate's address and submit the transaction. That's it! Your delegate is assigned. ### Remove a Delegate 1. Go to the **Contract** tab. ![](/img/etherscan-voting/Contract_6.png) 2. Go to the **Write as Proxy** tab. 3. Use the `unassignDelegate` method (number 2). 4. Click **Connect to Web3** and connect your address. The indicator should turn green. 5. Click **Write** without entering anything and sign the transaction. That's it! Your delegate has been removed. --- # Bridge Tokens via Jumpgate Jumpgates are a class of contracts that facilitate cross-chain token transfers under DAO operations. Each jumpgate is set up to work with a particular token and a pre-defined recipient. Below is the procedure of transferring tokens using a jumpgate. [**Watch video tutorial**](https://youtu.be/IqphF28aTUU) ### 1. Verify Jumpgate In this step we will be making sure that the jumpgate is correctly configured. You will only need to do this once because jumpgates are non-upgradeable contracts. Go to [Etherscan](https://etherscan.io/) and open the Jumpgate page. Click the "Contract" tab, the green check mark confirms that the source code is verified. Check the parameters: - `arbiterFee` is always 0; - `bridge` is the address of the bridge. Currently, all jumpgates use only Wormhole Token bridge at [`0x3ee18B2214AFF97000D974cf647E7C347E8fa585`](https://etherscan.io/address/0x3ee18B2214AFF97000D974cf647E7C347E8fa585), and you can check the address against the [Wormhole docs](https://book.wormhole.com/reference/contracts.html); - `nonce` is always 0; - `owner` is the Aragon Agent at [`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c) verifiable against [Deployed contracts](/deployed-contracts/#dao-contracts); - `recipient` is the recipient address in hexadecimal form. For Solana, this will be an encoded LDO token account. Use [Base 58 decoder](https://appdevtools.com/base58-encoder-decoder) to decode this hexadecimal sequence to the Solana address format. - `recipientChain` is the target chain identifier. If the Jumpgate is using Wormhole bridge, you can check the id against the [Wormhole docs](https://book.wormhole.com/reference/contracts.html), Solana id is 1; - `renounceOwnership` should yield an error; - `token` is the address of the token being transferred, e.g. LDO at [0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32](https://etherscan.io/address/0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32). Check the LDO address against [Deployed contracts](/deployed-contracts/#dao-contracts). ![](/img/jumpgates/read-contract.png) ### 2. Transfer tokens to Jumpgate The jumpgate is agnostic to how tokens were received. You can either transfer tokens directly or in the context of DAO operations via an Aragon vote or Easytrack transfer motion. ### 3. Bridge Tokens Now we can send tokens through the bridge. We cannot input the amount of tokens to bridge and the jumpgate will transfer the entirety of its token balance. Open "Write contract" tab and connect your wallet by clicking the "Connect to Web3" button. We will now expand `bridgeTokens` function and click "Write". Remember that this function is permissionless and you can initiate the transfer from any account as long as you have enough ether for gas. ![](/img/jumpgates/write-contract.png) ### 4. Claim tokens Claiming process may be different depending on the bridge but for now all jumpgates only support Wormhole Token Bridge. We will be using Portal Bridge (formerly Wormhole) website to claim tokens on Solana. - To go [Portal Bridge website](https://www.portalbridge.com/#/redeem) Redeem page and connect your Ethereum wallet. Select "Token" in "Type" dropdown and "Ethereum" in "Source Chain". Paste the hash of the `bridgeTokens` transaction. At first, this should produce an error because it takes some time for Portal Bridge to process the bridge transaction. Try this step again in 10-20 minutes and click "Recover" button. ![](/img/jumpgates/recover.png) - "Recover" will redirect you to "Tokens" tab, where you will be able to confirm the recipient address. Connect your Solana wallet, click "Redeem". You will be prompted to sign a few transactions. Once those are confirmed, you will be able to see the tokens on the recipient. ![](/img/jumpgates/redeem.png) --- # Keys API Simple Lido keys and validators HTTP API. ## Requirements 1. 2 core CPU 2. 5 GB RAM - Keys-API-DB โ€” 500MB - Keys-API โ€” 4GB 3. EL Full node 4. CL node for applications like the Ejector that use the [validators API](https://hackmd.io/fv8btyNTTOGLZI6LqYyYIg?view#validators). For Teku, please use the archive mode. Nimbus is currently not supported. ## Environment Variables An annotated env sample is available in the repository: https://github.com/lidofinance/lido-keys-api/blob/main/sample.env ## How to Run For running `Keys Api`, please use a stable version's image hash, [available here](/guides/tooling/). Below you can find a docker-compose example for running the service with a database. https://github.com/lidofinance/lido-keys-api/blob/main/docker-compose.yml To run using docker-compose: ```bash docker-compose up ``` Now you can access the API on `http://localhost:${PORT}/api`. ## Monitoring Prometheus metrics will be available on endpoint `http://localhost:${PORT}/metrics`. You can find configs and dashboards for running Prometheus and Grafana locally in the repository: [Grafana](https://github.com/lidofinance/lido-keys-api/tree/main/grafana), [Prometheus](https://github.com/lidofinance/lido-keys-api/tree/main/prometheus). Example of a `docker-compose.yml` with metrics setup: https://github.com/lidofinance/lido-keys-api/blob/main/docker-compose.metrics.yml ## Additional Resources Keys API GitHub Repository (Open Source) https://github.com/lidofinance/lido-keys-api API and internal logic documentation https://hackmd.io/@lido/B1aCdW6Lo --- # Late Prover Bot ## Introduction Late Prover Bot monitors the beacon chain for validator exit requests that have passed their required deadline. When a validator fails to exit on time, the bot generates a cryptographic Merkle proof of the delay and submits it to the `ValidatorExitDelayVerifier` smart contract, enabling penalty enforcement on the responsible node operator. ## Requirements ### Hardware - 1-core CPU - 8GB RAM ### Nodes - Ethereum EL RPC service - Ethereum CL API service (Beacon Node) ## How to use The bot runs as a daemon, continuously processing finalized beacon chain roots. For each root, it discovers `ValidatorsExitBusOracle` events in the corresponding EL block range, groups validators by their exit deadline slot, and generates proofs for any that have exceeded their deadline. Proofs are submitted in batches via `verifyValidatorExitDelay()` (or `verifyHistoricalValidatorExitDelay()` for older roots). ### Envs Required variables are (mainnet): | Variable | Default | Description | |---|---|---| | `CHAIN_ID` | - | Ethereum chain ID. `1` for mainnet, `560048` for Hoodi | | `EL_RPC_URLS` | - | Comma-separated list of EL RPC endpoints | | `CL_API_URLS` | - | Comma-separated list of CL Beacon API endpoints | | `LIDO_LOCATOR_ADDRESS` | - | Lido Locator contract address. Addresses for each network can be found [here](/deployed-contracts/) | | `TX_SIGNER_PRIVATE_KEY` | - | Private key used to sign and submit transactions. Not required when `DRY_RUN=true` | | `DRY_RUN` | `false` | If `true`, proofs are generated but transactions are not sent | Optional variables can be found [here](https://github.com/lidofinance/late-prover-bot#readme). ## Running ### Source Code 1. Clone repository and install requirements: ```bash git clone git@github.com:lidofinance/late-prover-bot.git cd late-prover-bot ``` 2. Install dependencies: ```bash yarn install yarn run typechain yarn build ``` 3. Run the bot: ```bash yarn run start:prod ``` ### Docker Docker image can be found [here](/guides/tooling/#late-prover-bot). ## Monitoring Prometheus metrics and health check are available on the same port: - `http://localhost:${HTTP_PORT}/metrics` - `http://localhost:${HTTP_PORT}/health` --- # Lido tokens integration guide This document is intended for developers looking to integrate Lido's stETH or wstETH tokens into their dApps or services, with a focus on money markets, DEXes and blockchain bridges. :::info The integration might be implemented on the level of smart contracts (on-chain) or [Lido on Ethereum SDK](/docs/integrations/sdk.md#lido-ethereum-sdk) (off-chain). ::: ## Lido Lido is a family of liquid staking protocols across multiple blockchains, with headquarters on Ethereum. Liquid refers to the ability of a userโ€™s stake to become liquid. Upon the user's deposit Lido issues stToken, which represents the deposited tokens along with all the rewards & penalties accrued through the deposit's staking. Unlike the staked funds, this stToken is liquid โ€” it can be freely transferred between parties, making it usable across different DeFi applications while still receiving daily staked rewards. It is paramount to preserve this property when integrating stTokens into any DeFi protocol. This guide refers to Lido on Ethereum (hereinafter referred to as Lido). ## Lido tokens ### stTokens: stETH and wstETH Staking ether with Lido gives an equivalent amount of [stETH](#steth). The user's stETH balance represents the amount of ether withdrawable directly from the Lido protocol. For easier DeFi integrations, `stETH` has a non-rebasable, value-accruing counterpart called ['wrapped stETH'](#wsteth) (or just `wstETH`). stETH (and therefore wstETH) can be obtained not only via direct staking in Lido Core and wrapping, but also via **Lido V3 stVaults (Staking Vaults)**: vault owners can mint `stETH` or `wstETH` backed by an stVault. **stETH minted via stVaults is the same canonical stETH token** as stETH minted via Lido Core. See [/run-on-lido/stvaults/](/run-on-lido/stvaults/) (especially the [integration overview](/run-on-lido/stvaults/tech-documentation/integration-overview)). Lido's ERC-20 compatible stTokens are widely adopted across the Ethereum ecosystem: - The most important on-chain [liquidity venues](https://dune.com/lido/wsteth-liquidity) include: - [stETH/ETH liquidity pool on Curve](https://curve.fi/#/ethereum/pools/steth) - [wstETH/ETH pool on Uniswap V3](https://app.uniswap.org/explore/pools/ethereum/0x109830a1AAaD605BbF02a9dFA7B0B92EC2FB7dAa) - [wstETH/ETH Composable stable pool on Balancer v2](https://app.balancer.fi/#/ethereum/pool/0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2) - wstETH is listed as a collateral token on the following AAVE v3 markets: - [Ethereum mainnet](https://app.aave.com/reserve-overview/?underlyingAsset=0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0&marketName=proto_mainnet_v3) - [Arbitrum](https://app.aave.com/reserve-overview/?underlyingAsset=0x5979d7b546e38e414f7e9822514be443a4800529&marketName=proto_arbitrum_v3) - [Base](https://app.aave.com/reserve-overview/?underlyingAsset=0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452&marketName=proto_base_v3) - [Optimism](https://app.aave.com/reserve-overview/?underlyingAsset=0x1f32b1c2345538c0c6f582fcb022739c4a194ebb&marketName=proto_optimism_v3) - wstETH is [listed as a collateral token on Maker](https://daistats.com/#/collateral) - there are various [Mellow LRT](https://app.mellow.finance/restake) projects built on top of the (w)stETH - steCRV (the Curve stETH/ETH LP token) is [listed as a collateral token on Maker](https://daistats.com/#/collateral) - Blast L2 integrated [stETH](https://docs.blastfutures.com/get-started/introduction/what-is-blast#how-blast-works) as a rebasable ether (being staked implicitly as a part of the L1->L2 ether bridging flow) - there are multiple liquidity strategies built on top of Lido's stTokens, including [Yearn](https://yearn.fi/vaults/1/0xdCD90C7f6324cfa40d7169ef80b12031770B4325) and [Harvest Finance](https://harvest.finance/) #### Integration utilities: Rate and price feeds The current sentiment for the money markets and DeFi integrations in general is to consider Liquid Staked Tokens being backed by their native exchange rates against ETH. This approach implies 1 stETH = 1 ETH pricing invariant to be used. Real world applications include [AAVE v3](https://github.com/bgd-labs/aave-proposals/blob/main/src/AaveV2-V3PriceFeedsUpdate_20230613/PRICE-FEEDS-UPDATE-20230613.md#motivation) markets and [Mellow LRT](https://etherscan.io/address/0x1Dc89c28e59d142688D65Bd7b22C4Fd40C2cC06d) pricing approaches. More in depth analysis is available [here](https://www.comp.xyz/t/franklin-dao-request-for-comment-on-market-pricing-vs-exchange-rate-pricing-for-lsts-and-potential-oracle-implementations/5130). There are following `wstETH/stETH` rate feeds available to use in conjunction with (w)stETH: For an up-to-date list of networks and feed addresses, see [deployed contracts](/deployed-contracts/#price-feeds). - [Ethereum Mainnet](https://etherscan.io/address/0x94336dF517036f2Bf5c620a1BC75a73A37b7bb16#readContract) - [Arbitrum](https://data.chain.link/feeds/arbitrum/mainnet/wsteth-steth%20exchangerate) - [Optimism](https://data.chain.link/feeds/optimism/mainnet/wsteth-steth%20exchangerate) - [Base](https://data.chain.link/feeds/base/base/wsteth-steth%20exchangerate) - [Linea](https://lineascan.build/address/0x3C8A95F2264bB3b52156c766b738357008d87cB7) - [BNB Chain](https://bscscan.com/address/0x4c75d01cfa4D998770b399246400a6dc40FB9645) :::note The Ethereum Mainnet Chainlink-compatible feed is deployed and used by the Mellow LRT vaults, being a wrapper for `wstETH.getStETHByWstETH(10 ** decimals)` ::: These feeds might be used to compose a target feed, e.g., for the `wstETH/USD` pair, see the following examples of AAVE v3 markets: - [Ethereum Mainnet `WstETHSynchronicityPriceAdapter`](https://etherscan.io/address/0x8b6851156023f4f5a66f68bea80851c3d905ac93#code) - [Optimism `CLSynchronicityPriceAdapterPegToBase`](https://optimistic.etherscan.io/address/0x80f2c02224a2e548fc67c0bf705ebfa825dd5439) - [Arbitrum `CLSynchronicityPriceAdapterPegToBase`](https://arbiscan.io/address/0x945fd405773973d286de54e44649cc0d9e264f78) ### LDO [LDO](#ldo-1) is a Lido governance ERC-20 compliant token derived from the [MiniMe Token](https://github.com/Giveth/minime). Thus, LDO holder balances are queryable for an arbitrary block number, an essential security feature for the Lido voting mechanics. ### unstETH A non-fungible token (NFT) is used to represent a withdrawal request position [in the protocol-level withdrawals queue](/contracts/withdrawal-queue-erc721) when a stToken holder decides to redeem it for ether via the protocol. :::note Unlike the other Lido's tokens (`stETH`, `wstETH`, and `LDO`), [unstETH](#withdrawals-unsteth) is non-fungible, and implements the ERC-721 token standard instead of ERC-20. ::: ## stETH vs. wstETH There are two versions of Lido's stTokens, namely stETH and wstETH. Both are fungible tokens but they reflect the accrued staking rewards differently. stETH implements rebasing mechanics which means the stETH balance updates regularly. On the contrary, the wstETH balance does not change on its own but rather increases in value against stETH. :::info At any moment, any amount of stETH can be converted to wstETH via a trustless wrapper and vice versa, thus tokens effectively share liquidity. ::: ### Aave V2 integration lesson Aave V2 integrated rebasable stETH directly. Its standard aToken accounting tracked an Aave liquidity index, so passing stETH rebases through to depositors required a [custom AStETH implementation](https://etherscan.io/address/0xbd233D4ffdAA9B7d1d3E6b18CCcb8D091142893a#code) that applied both the liquidity index and a stETH share-based rebasing index. This extra conversion layer made nominal stETH and aSTETH amounts subject to wei-level rounding: deposits could mint slightly less aSTETH than the requested stETH amount, and exact-amount flows had to account for the [1โ€“2 wei stETH transfer corner case](#1-2-wei-corner-case). The integration received a [dedicated security audit](https://github.com/lidofinance/audits/blob/main/MixBytes%20AAVE%20stETH%20integration%20Security%20Audit%20Report%2002-22.pdf). This history is an integration-design lesson, not a loss of stETH composability. [wstETH](#what-is-wsteth) is a trustless wrapper around the same stETH and can be converted back to stETH. It converts the rebasing accounting model into a value-accruing ERC-20 representation: holder balances stay static while each wstETH represents a changing amount of stETH. This fits protocols whose accounting assumes balances change only on transfers, minting, or burning, avoiding a custom rebasing adapter. [Aave V3](https://aave.com/blog/lido-aave-case-study) and [Aave V4](https://governance.aave.com/t/arfc-aave-v4-activation-on-ethereum-mainnet/24293) use wstETH as collateral. Many lending and broader DeFi integrations follow the same pattern; see the [current examples](#sttokens-steth-and-wsteth). Integrate rebasable stETH when the application intentionally supports its share and rebase semantics; otherwise, prefer wstETH. For instance, undercollateralized wstETH positions on Maker can be liquidated by unwrapping wstETH and swapping it for ether on Curve. ## stETH ### What is stETH stETH is a rebasable ERC-20 token that represents ether staked with Lido. Unlike staked ether, it is liquid and can be transferred, traded, or used in DeFi applications. The total supply of stETH reflects the amount of ether deposited into protocol combined with staking rewards, minus potential validator penalties. stETH tokens are minted upon ether deposit at 1:1 ratio. Since withdrawals from the Consensus Layer have been introduced, it is also possible to redeem ether by burning stETH at the same 1:1 ratio (in rare cases it won't preserve 1:1 ratio though). Please note, Lido has implemented staking rate limits aimed at reducing the post-Merge staking surge's impact on the staking queue & Lidoโ€™s socialized rewards distribution model. Read more about it [here](#staking-rate-limits). stETH is a rebasable ERC-20 token. Normally, the stETH token balances get recalculated daily when the Lido oracle reports the Consensus Layer ether balance update. The stETH balance update happens automatically on all the addresses holding stETH at the moment of rebase. The rebase mechanics have been implemented via shares (see [shares](#steth-internals-share-mechanics)). ### Note on ERC-20 compliance stETH does not strictly comply with ERC-20. The only exception is that it does not emit `Transfer()` on rebase as [ERC-20](https://eips.ethereum.org/EIPS/eip-20#events) standard requires. ### Accounting oracle Normally, stETH rebases happen daily when the Lido oracle reports the Consensus Layer ether balance update. The rebase can be positive or negative, depending on the validators' performance. In case Lido's validators get slashed or penalized, the stETH balances can decrease according to penalty sizes. However, daily rebases have never been negative by the time of writing. The accounting oracle has sanity checks on both max APR reported (the APR cannot exceed 27%, which means a daily rebase is limited to `(27/365)%`) and total staked amount drop (staked ether decrease reported cannot exceed 5%). Currently, Oracle network includes 9 independent oracles, oracle daemons hosted by established node operators selected by the DAO. As soon as five out of nine oracle daemons report the same data, reaching the consensus, the report goes to the Lido smart contract, and the rebase occurs. #### Oracle corner cases - In case oracle daemons do not report Consensus Layer balance update or do not reach quorum, the oracle does not submit the daily report, and the daily rebase doesn't occur until the quorum is reached. - Oracle report might be delayed, but it will include values actual for the reporting refSlot. So, even if reported 2 hours late, it will include only rebase values for the original period. - In case the quorum hasn't been reached, the oracle can skip the daily report. The report will happen as soon as the quorum for one of the next periods will be reached, and it will include the incremental balance update for all periods since the last successful oracle report. - Oracle daemons only report the finalized epochs. In case of no finality on the Consensus Layer, the daemons won't submit their reports, and the daily rebase won't occur. - In case sanity checks on max APR or total staked amount drop fail, the oracle report cannot be finalized, and the rebase cannot happen. ### stETH internals: share mechanics Daily rebases result in stETH token balances changing. This mechanism is implemented via shares. The `share` is a basic unit representing the stETH holder's share in the total amount of ether controlled by the protocol. When a new deposit happens, the new shares get minted to reflect what share of the protocol-controlled ether has been added to the pool. When the Consensus Layer oracle report comes in, the price of 1 share in stETH is being recalculated. Shares aren't normalized, so the contract also stores the sum of all shares to calculate each account's token balance. Shares balance by stETH balance can be calculated by this formula: ```js shares[account] = balanceOf(account) * totalShares / totalPooledEther ``` #### 1-2 wei corner case stETH balance calculation includes integer division, and there is a common case when the whole stETH balance can't be transferred from the account while leaving the last 1-2 wei on the sender's account. The same thing can actually happen at any transfer or deposit transaction. In the future, when the stETH/share rate will be greater, the error can become a bit bigger. To avoid it, one can use `transferShares` to be precise. Example: 1. User A transfers 1 stETH to User B. 2. Under the hood, stETH balance gets converted to shares, integer division happens and rounding down applies. 3. The corresponding amount of shares gets transferred from User A to User B. 4. Shares balance gets converted to stETH balance for User B. 5. In many cases, the actually transferred amount is 1-2 wei less than expected. The issue is documented here: [lido-dao/issues/442](https://github.com/lidofinance/lido-dao/issues/442) ### Bookkeeping shares Although user-friendly, stETH rebases add a whole level of complexity to integrating stETH into other dApps and protocols. When integrating stETH as a token into any dApp, it's highly recommended to store and operate shares rather than stETH public balances directly, because stETH balances change both upon transfers, mints/burns, and rebases, while shares balances can only change upon transfers and mints/burns. To figure out the shares balance, `getSharesByPooledEth(uint256)` function can be used. It returns the value not affected by future rebases and it can be converted back into stETH by calling `getPooledEthByShares` function. > See all available stETH methods [here](https://github.com/lidofinance/docs/blob/main/docs/contracts/lido.md#view-methods). Any operation on stETH can be performed on shares directly, with no difference between share and stETH. The preferred way of operating stETH should be: 1) get stETH token balance; 2) convert stETH balance into shares balance and use it as a primary balance unit in your dApp; 3) when any operation on the balance should be done, do it on the shares balance; 4) when users interact with stETH, convert the shares balance back to stETH token balance. Please note that 10% APR on shares balance and 10% APR on stETH token balance will ultimately result in different output values over time, because shares balance is stable, while stETH token balance changes eventually. There are two convenience methods to work with shares available for the stETH token: - `transferShares` (when `msg.sender` spends their own balance) - `transferSharesFrom` (when `msg.sender` spends the approved allowance) If using the rebasable stETH token is not an option for your integration, it is recommended to use wstETH instead of stETH. See how it works [here](#wsteth). ### Transfer shares function for stETH The [LIP-11](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-11.md) introduced the `transferShares` function which allows to transfer stETH in a "rebase-agnostic" manner: transfer in terms of [shares](#steth-internals-share-mechanics) amount. Normally, one transfers stETH using ERC-20 `transfer` and `transferFrom` functions which accept as an input the amount of stETH, not the amount of the underlying shares. Sometimes it's better operate with shares directly to avoid possible rounding issues. Rounding issues usually could appear after a token rebase. This feature is aimed to provide an additional level of precision when operating with stETH. Read more about the function in the [LIP-11](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-11.md). Also, V2 upgrade introduced a `transferSharesFrom` to completely match ERC-20 set of transfer methods. ### Fees Lido collects a percentage of the staking rewards as a protocol fee. The exact fee size is defined by the DAO and can be changed in the future via DAO voting. To collect the fee, the protocol mints new stETH token shares and assigns them to the fee recipients. Currently, the fee collected by Lido protocol is 10% of staking rewards with half of it going to the node operators and the other half going to the protocol treasury. Since the total amount of Lido pooled ether tends to increase, the combined value of all holders' shares denominated in stETH increases respectively. Thus, the rewards effectively spread between each token holder proportionally to their share in the protocol TVL. So Lido mints new shares to the fee recipient so that the total cost of the newly-minted shares exactly corresponds to the fee taken (calculated in basis points): ``` shares2mint * newShareCost = (_totalRewards * feeBasis) / 10000 newShareCost = newTotalPooledEther / (prevTotalShares + shares2mint) ``` which follows: ``` _totalRewards * feeBasis * prevTotalShares shares2mint = -------------------------------------------------------------- (newTotalPooledEther * 10000) - (feeBasis * _totalRewards) ``` ### How to get APR? Please refer to [this page](/integrations/api/#last-lido-apr-for-steth) for the correct Lido V2 APR calculation. It is worth noting that with withdrawals enabled, the APR calculation method for Lido has changed significantly. When Lido V2 protocol finalizes withdrawal requests, the Lido contract excludes funds from TVL and assigns to burn underlying locked requestsโ€™ stETH shares in return. In other words, withdrawal finalization decreases both TVL and total shares. The old V1 formula isnโ€™t suitable anymore because it catches TVL changes, but skips total shares changes. ### Do stETH rewards compound? Yes, stETH rewards do compound. All rewards that are withdrawn from the Consensus Layer or received as MEV or EL priority fees (that aren't used to fulfill withdrawal requests) are finally restaked to set up new validators and receive more rewards at the end. So, we can say that stETH becomes fully auto-compounding after V2 release. ## wstETH Due to the rebasing nature of stETH, the stETH balance on the holder's address is not constant, it changes daily as oracle report comes in. Although rebasable tokens are becoming a common thing in DeFi recently, many dApps do not support rebasing. For example, Maker, UniSwap, and SushiSwap are not designed for rebasable tokens. Listing stETH on these apps can result in holders not receiving their daily staking rewards which effectively defeats the benefits of liquid staking. To integrate with such dApps, there's another form of Lido stTokens called wstETH (wrapped staked ether). ### What is wstETH wstETH is an ERC20 token that represents the account's share of the stETH total supply (stETH token wrapper with static balances). For wstETH, 1 wei in [shares](#steth-internals-share-mechanics) equals to 1 wei in balance. The wstETH balance can only be changed upon transfers, minting, and burning. wstETH balance does not rebase, wstETH's price denominated in stETH changes instead. At any given time, anyone holding wstETH can convert any amount of it to stETH at a fixed rate, and vice versa. The rate is the same for everyone at any given moment. Normally, the rate gets updated once a day, when stETH undergoes a rebase. The current rate can be obtained by calling `wstETH.stEthPerToken()` or `wstETH.getStETHByWstETH(10 ** decimals)`. ### Wrap & Unwrap When wrapping stETH to wstETH, the desired amount of stETH is locked on the WstETH contract balance, and the wstETH is minted according to the [share bookkeeping](#bookkeeping-shares) formula. When unwrapping, wstETH gets burnt and the corresponding amount of stETH gets unlocked. Thus, the amount of stETH unlocked when unwrapping is different from what has been initially wrapped (given a rebase happened between wrapping and unwrapping stETH). #### wstETH shortcut Note, that the WstETH contract includes a shortcut to convert ether to wstETH under the hood, which allows you to effectively skip the wrapping step and stake ether for wstETH directly. Keep in mind that when using the shortcut, [the staking rate limits](#staking-rate-limits) still apply. #### `wstETHReferralStaker`: stake directly into wstETH with referral If you need to stake ETH into Lido and receive `wstETH` in **one transaction** (while also providing a `referral` address), use the permissionless `wstETHReferralStaker` helper contract. ::::warning Do not send ETH or tokens directly to `wstETHReferralStaker`. Use its payable `stakeETH(address _referral)` method. :::: See: [`wstETHReferralStaker`](/contracts/wsteth-staker). ### Rewards accounting Since wstETH represents the holder's share in the total amount of Lido-controlled ether, rebases don't affect wstETH balances but change the wstETH price denominated in stETH. **Basic example**: 1. User wraps 1 stETH and gets 0.9803 wstETH (1 stETH = 0.9803 wstETH) 2. A rebase happens, the wstETH price goes up by 5% 3. User unwraps 0.9803 wstETH and gets 1.0499 stETH (1 stETH = 0.9337 wstETH) ### Hoodi wstETH for testing The most recent testnet version of the Lido protocol lives on the Hoodi testnet (see the full list of contracts [here](/deployed-contracts/hoodi)). Just like on mainnet, Hoodi wstETH for testing purposes can be obtained by approving the desired amount of stETH to the WstETH contract on Hoodi, and then calling `wrap` method on it. The corresponding amount of Hoodi stETH will be locked on the WstETH contract, and the wstETH tokens will be minted to your account. Hoodi ether can also be converted to wstETH directly using the [wstETH shortcut](#wsteth-shortcut) โ€“ just send your Hoodi ether to WstETH contract on Hoodi, and the corresponding amount of wstETH will be minted to your account. ::::note Sepolia is deprecated and no longer used for Lido token testing. Use Hoodi for testnet integrations. :::: ### Lido Multichain #### wstETH Currently, wstETH token is present on multiple networks (see [deployed contracts](/deployed-contracts/#lido-multichain)): - [Arbitrum](https://arbiscan.io/address/0x5979D7b546E38E414F7E9822514be443A4800529) - [Optimism](https://optimistic.etherscan.io/address/0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb) - [Base](https://basescan.org/address/0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452) - [Linea](https://lineascan.build/address/0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F) - [Binance Smart Chain (BSC)](https://bscscan.com/address/0x26c5e01524d2E6280A48F2c50fF6De7e52E9611C) - [Unichain](https://uniscan.xyz/address/0xc02fE7317D4eb8753a02c35fe019786854A92001) with bridging implemented via [the canonical bridges recommended approach](/docs/token-guides/cross-chain-tokens-guide.md). :::note On most networks, wstETH for Lido Multichain is a bridged ERC-20 token and cannot be unwrapped locally. On networks where stETH is also available, the token design follows the [LIP-22](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-22.md) approach. ::: Without the shares bookkeeping, the bridged token cannot provide the `wstETH/stETH` rate and the rewards accrued on-chain. Use the [wstETH/stETH rate feeds](#integration-utilities-rate-and-price-feeds) listed above. #### stETH (OP Stack networks) stETH is available on some OP Stack networks alongside wstETH (see [deployed contracts](/deployed-contracts/#lido-multichain)). The wstETH and stETH tokens design follows the [LIP-22](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-22.md) architecture approach. - Optimism: - Token address: [`0x76A50b8c7349cCDDb7578c6627e79b5d99D24138`](https://optimistic.etherscan.io/address/0x76A50b8c7349cCDDb7578c6627e79b5d99D24138) - wstETH/stETH in-protocol native rate feed: [`0x294ED1f214F4e0ecAE31C3Eae4F04EBB3b36C9d0`](https://optimistic.etherscan.io/address/0x294ED1f214F4e0ecAE31C3Eae4F04EBB3b36C9d0) - Unichain: - Token address: [`0x81f2508AAC59757EF7425DDc9717AB5c2AA0A84F`](https://uniscan.xyz/address/0x81f2508AAC59757EF7425DDc9717AB5c2AA0A84F) - wstETH/stETH in-protocol native rate feed: [`0xD835fAC9080396CCE95bDf9EcC7cc27Bab12c9f8`](https://uniscan.xyz/address/0xD835fAC9080396CCE95bDf9EcC7cc27Bab12c9f8) The native rate feed allows getting `wstETH/stETH` in-protocol rate delivered from the L1 side by the canonical bridge. ## LDO ### What is LDO LDO is a governance token used for the Lido DAO's voting process ([both off-chain and on-chain](https://lido.fi/governance#regular-process)). The token is widely available in DeFi and CeFi ecosystems. LDO has internal mechanics of the balance snapshots ([`balanceOfAt`](https://etherscan.io/address/0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32#readContract#F5) and [`totalSupplyAt`](https://etherscan.io/address/0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32#readContract#F10)) to allow voting power not being manipulated within the time of the ongoing vote. ### Note on ERC-20 compliance Although the LDO is fully compliant with ERC-20, it is worth noting that the token doesn't revert a transaction on all of the failure paths inside both `transfer` and `transferFrom` methods returning the `false` status instead. :::note It's critical to check the return status for external integrations as the ERC-20 token standard [requires](https://eips.ethereum.org/EIPS/eip-20#methods) to prevent various attack vectors (e.g. token deposits in vaults): > Callers MUST handle `false` from `returns (bool success)`. Callers MUST NOT assume that `false` is never returned! ::: ## ERC20Permit wstETH and stETH Ethereum Mainnet tokens implement the ERC20 Permit extension allowing approvals to be made via signatures, as defined in [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612). stETH is also compatible with smart contract signatures, implementing [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) that is used as a part of the Account Abstraction. The `permit` method allows users to modify the allowance using a signed message, instead of through `msg.sender`. By not relying on `approve` method, you can build interfaces that will approve and use wstETH in one tx. ## Staking rate limits In order to handle the staking surge in case of some unforeseen market conditions, the Lido protocol implemented staking rate limits aimed at reducing the surge's impact on the staking queue & Lidoโ€™s socialized rewards distribution model. There is a sliding window limit that is parametrized with `_maxStakingLimit` and `_stakeLimitIncreasePerBlock`. This means it is only possible to submit this much ether to the Lido staking contracts within a 24-hours timeframe. The exact limit can change over time; read it on-chain via `getCurrentStakeLimit()` (or `getStakeLimitFullInfo()`). You can picture this as a health globe from Diablo 2 with a maximum of `_maxStakingLimit` and regenerating with a constant speed per block. When you deposit ether to the protocol, the level of health is reduced by its amount and the current limit becomes smaller and smaller. When it hits the ground, the transaction gets reverted. To avoid that, you should check if `getCurrentStakeLimit() >= amountToStake`, and if it's not you can go with an alternative route. The staking rate limits are denominated in ether, thus, it makes no difference if the stake is being deposited for stETH or using [the wstETH shortcut](#wsteth-shortcut), the limits apply in both cases. ### Alternative routes 1. Wait for staking limits to regenerate to higher values and retry depositing ether to Lido later. 2. Consider swapping ETH for stETH on DEXes like Curve or Balancer. At specific market conditions, stETH may effectively be purchased from there with a discount due to stETH price fluctuations. ## Withdrawals (unstETH) Lido V2 introduced the possibility to withdraw ETH from the Lido on Ethereum protocol (i.e., primary market). :::note As in-protocol withdrawals have asynchronous nature and sophisticated execution flow, in general, using secondary markets (exchanges and swap aggregators) might be more UX-friendly and convenient option to consider for integrations. ::: A high-level upgrade overview can be found in [the blog post](https://blog.lido.fi/introducing-lido-v2/). Withdrawals flow is organized as a FIFO queue that accepts the requests with stETH attached and these requests are finalized with oracle reports as soon as ether to fulfill the request is available. So to obtain ether from the protocol, you'll need to proceed with the following steps: - request the withdrawal, locking your steth in the queue and receiving an NFT, that represents your position in the queue - wait, until the request is finalized by the oracle report and becomes claimable - claim your ether, burning the NFT Request size should be at least **100 wei** (in stETH) and at most **1000 stETH**. Larger amounts should be withdrawn in multiple requests, which can be batched via in-protocol API. Once requested, withdrawal cannot be canceled. The withdrawal NFT can be transferred to a different address, and the new owner will be able to claim the requested withdrawal once finalized. The amount of claimable ETH is determined once the withdrawal request is finalized. The rate stETH/ETH of the request finalization can't get higher than it's been at the moment of request creation. The user will be able to claim: - normally โ€“ the ETH amount corresponding to the stETH amount at the moment of the request's placement **OR** - discounted - lowered ETH amount corresponding to the oracle-reported share rate in case the protocol had undergone significant losses (slashings and penalties) The second option is unlikely, and we haven't ever seen the conditions for it on mainnet so far. The end-user contract to deal with the withdrawals is `WithdrawalQueueERC721.sol`, which implements the ERC721 standard. NFT represents the position in the withdrawal queue and may be claimed after the finalization of the request. Let's follow these steps in detail: ### Request withdrawal and mint NFT You have several options for requesting withdrawals, they require you to have stETH or wstETH on your address: #### stETH - Call `requestWithdrawalsWithPermit(uint256[] _amounts, address _owner, PermitInput _permit)` and get the ids of created positions, where `msg.sender` will be used to transfer tokens from and the `_owner` will be the address that can claim or transfer NFT (defaults to `msg.sender` if itโ€™s not provided) - Alternatively, sending stETH on behalf of `WithdrawalQueueERC721.sol` contract can be approved in a separate upfront transaction (`stETH.approve(withdrawalQueueERC721.address, allowance)`), and the `requestWithdrawals(uint256[] _amounts, address _owner)` method called afterwards #### wstETH - Call `requestWithdrawalsWstETHWithPermit(uint256[] _amounts, address _owner, PermitInput _permit)` and get the ids of created positions, where `msg.sender` will be used to transfer tokens from, and the `_owner` will be the address that can claim or transfer NFT (defaults to `msg.sender` if itโ€™s not provide) - Alternatively, sending wstETH on behalf of `WithdrawalQueueERC721.sol` contract can be approved in a separate upfront transaction (`wstETH.approve(withdrawalQueueERC721.address, allowance)`), and the `requestWithdrawalsWstETH(uint256[] _amounts, address _owner)` method called afterwards `PermitInput` structure is defined as follows: ```solidity struct PermitInput { uint256 value; uint256 deadline; uint8 v; bytes32 r; bytes32 s; } ``` After request, [ERC721](https://eips.ethereum.org/EIPS/eip-721) NFT is minted to `_owner` address and can be transferred to the other owner who will have all the rights to claim the withdrawal. Additionally, this NFT implements the [ERC4906](https://eips.ethereum.org/EIPS/eip-4906) standard and it's recommended to rely on ```solidity event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); ``` to update the NFT metadata if you're integrating it somewhere where it should be displayed correctly. :::note Withdrawal transactions made with `requestWithdrawalsWithPermit` or `requestWithdrawalsWstETHWithPermit` might fail due to being front-run by stealing the user-provided signature to execute `token.permit` method. It does not impose any fund loss risks nor blocks the capability to withdraw, but it affects the UX. For the details, see [this issue](https://github.com/lidofinance/lido-dao/issues/803). It's recommended to mitigate the issue, e.g. by utilizing the approach used in [Lido staking widget](https://github.com/lidofinance/ethereum-staking-widget). Shortly, the idea is as follows. If the initial `...WithPermit` transaction fails, immediately resent the request but via `requestWithdrawals/requestWithdrawalsWstETH` method this time, seamlessly relying on the allowance already provided as a result of the griefing transaction. For the specific example, see [the following code](https://github.com/lidofinance/ethereum-staking-widget/blob/ba65f2180ad0ab43b5f3bdcfeee118e6ceeabe7f/features/withdrawals/hooks/contract/useRequest.ts#L319C6-L319C6). Any other viable approach for mitigation might be used as well. As one more example, deploy a wrapper smart contract that tries `requestWithdrawalsWithPermit/requestWithdrawalsWithPermitWstETH` and if [catches](https://docs.soliditylang.org/en/latest/control-structures.html#try-catch) the revert error, continues with `requestWithdrawals/requestWithdrawalsWstETH`, checking the allowance is enough. ::: ### Checking the state of withdrawal - You can check all the withdrawal requests for the owner by calling `getWithdrawalRequests(address _owner)` which returns an array of NFT ids. - To check the state of the particular NFTs you can call `getWithdrawalStatus(uint256[] _requestIds)` which returns an array of [`WithdrawalRequestStatus`](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/WithdrawalQueueBase.sol#L67-L81) struct. ```solidity struct WithdrawalRequestStatus { /// @notice stETH token amount that was locked on withdrawal queue for this request uint256 amountOfStETH; /// @notice amount of stETH shares locked on withdrawal queue for this request uint256 amountOfShares; /// @notice address that can claim or transfer this request address owner; /// @notice timestamp of when the request was created, in seconds uint256 timestamp; /// @notice true, if request is finalized bool isFinalized; /// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed) bool isClaimed; } ``` >NOTE: Since stETH is an essential token if the user requests a withdrawal using wstETH directly, the amount will be nominated in stETH on request creation. You can call `getClaimableEther(uint256[] _requestIds, uint256[] _hints)` to get the exact amount of eth that is reserved for the requests, where `_hints` can be found by calling `findCheckpointHints(__requestIds, 1, getLastCheckpointIndex())`. It will return a non-zero value only if the request is claimable (`isFinalized && !isClaimed`) ### Claiming To claim ether you need to call: - `claimWithdrawal(uint256 _requestId)` with the NFT Id on behalf of the NFT owner - `claimWithdrawals(uint256[] _requestIDs, uint256[] _hints)` if you want to claim multiple withdrawals in batches or optimize on hint search - hints = `findCheckpointHints(uint256[] calldata _requestIDs, 1, lastCheckpoint)` - lastCheckpoint = `getLastCheckpointIndex()` ## General integration examples ### stETH/wstETH as collateral stETH/wstETH as DeFi collateral is beneficial for several reasons: - stETH/wstETH is almost as safe as ether, price-wise: barring catastrophic scenarios, its value tends to hold the ETH 1:1 well; - stETH/wstETH is a productive token: getting rewards on collateral effectively lowers the cost of borrowing; - stETH/wstETH is a very liquid token with billions of liquidity locked in liquidity pools (see [above](#sttokens-steth-and-wsteth)) Lido's staked tokens have been listed on major liquidity protocols: - On Maker, [wstETH collateral (scroll down to Dai from WSTETH-A section)](https://daistats.com/#/collateral) can be used to mint DAI stablecoin. See [Lido's blog post](https://blog.lido.fi/makerdao-integrates-lidos-staked-eth-steth-as-collateral-asset/) for more details. - On AAVE v3, multiple tokens can be borrowed against wstETH on various chains (see the list of the [markets](#sttokens-steth-and-wsteth)) Robust price sources are required for listing on most money markets, with ChainLink price feeds being the industry standard. The default option to use is exchange [rate feeds](#integration-utilities-rate-and-price-feeds) with an option to compose arbitrary feeds: ```python 'wstETH/X price feed' = 'wstETH/stETH rate feed' ร— 'ETH/X price feed' ``` ### Wallet integrations Lido's Ethereum staking services have been successfully integrated into the most popular DeFi wallets, including Ledger, Metamask, MyEtherWallet, ImToken and others. Having stETH integrated can provide wallet users with a great user experience of direct staking from the wallet UI itself. When adding stETH support to a DeFi wallet, it is important to preserve stETH's rebasing nature. Note that stETH balance changes on each rebase without any incoming or outgoing user transfers and does not emit ERC-20 'Transfer' events. As a consequence, avoid storing cached stETH balance for extended periods of time (over 24 hours). The integration might be implemented leveraging the [Lido on Ethereum SDK](/docs/integrations/sdk.md#lido-ethereum-sdk) ### Cross chain bridging The Lido's wstETH gets bridged to various L2's and sidechains. The process of a new network adoption in a future-proof way is outlined as a part of the separate [bridging guide](/docs/token-guides/cross-chain-tokens-guide.md). Most cross-chain token bridges have no mechanics to handle rebases. This means bridging stETH to other chains will prevent stakers from collecting their staking rewards. :::warning In the most common case, the rewards will naturally go to the bridge smart contract becoming locked there and never make it to the stakers. ::: While working on full-blown bridging solutions, the Lido contributors encourage the users to only bridge the non-rebasable representation of staked ether, namely wstETH. ## Risks There exist a number of potential risks when staking using liquid staking protocols. ### Smart contract security There is an inherent risk that Lido could contain a smart contract vulnerability or bug. The Lido code is open-source, audited, and covered by an extensive bug bounty program to minimize this risk. To mitigate smart contract risks, all of the core Lido contracts are audited. Audit reports can be found [here](https://github.com/lidofinance/audits). Besides, Lido is covered with a massive Immunefi bug bounty program. ### Slashing risk Validators risk staking penalties, with up to 100% of staked funds at risk if validators fail. To minimize this risk, Lido stakes across multiple professional and reputable node operators with heterogeneous setups, with additional mitigation in the form of self-coverage. ### stToken price risk Users risk an exchange price of stTokens which is lower than inherent value due to withdrawal restrictions on Lido, making arbitrage and risk-free market-making impossible. The Lido DAO is driven to mitigate the above risks to the extent possible. Despite this, they may still exist and, as such, it is our duty to communicate them. You can find an extensive [Public Risk Disclosure](/prd) on a dedicated documentation page. --- # Multisig deployment :::warning This page is massively outdated with the latest [Lido V2 release](https://github.com/lidofinance/lido-dao/releases/tag/v2.0.0). ::: This HOWTO describes deployment of the DAO using a multisig/airgapped signer, step-by-step. ## Preparation Clone the repo and install the deps: ```text $ git clone git@github.com:lidofinance/lido-dao.git $ cd lido-dao $ yarn ``` Running deployment scripts requires RPC connection to an Ethereum client, which can be configured by editing the `hardhat.config.js` file. It is already pre-configured for using the Infura provider, just copy `accounts.sample.json` to `accounts.json` and edit the `infura` key: ```json { "eth": {}, "infura": { "projectId": "PUT_YOUR_PROJECT_ID_HERE" } } ``` Some of the deployment steps (namely, deploying contracts) cannot be performed from some multisig providers and thus require sending the transactions from a usual address. The repo provides a helper for doing that; if you plan to use it, edit `accounts.json` and put your accounts config under the `eth.` key. If your RPC client provides an unlocked account, use `remote` as the value (here and later we assume that the target network is named `mainnet`): ```json { "eth": { "mainnet": "remote" }, "infura": { "projectId": "PUT_YOUR_PROJECT_ID_HERE" } } ``` If you plan to use a BIP-44 mnemonic phrase instead, use the following config shape: ```json { "eth": { "mainnet": { "mnemonic": "YOUR_MNEMONIC_HERE", "path": "m/44'/60'/0'/0", "initialIndex": 0, "count": 1 } }, "infura": { "projectId": "PUT_YOUR_PROJECT_ID_HERE" } } ``` You can test the config correctness by listing the accounts and their balances: ```text $ yarn hardhat --network mainnet list-accts ``` ## Deployment steps The deployment process consists of multiple steps. Generally, after each step a set of transaction files is generated. These transactions need to be executed in a sequential order: only send the next transaction after the previous one is included in a block. After the last transaction from a certain step is included in a block, move to the next step. There's also a couple steps that don't generate any transactions but check the correctness of the previous steps instead. ## 1. Deploying the base implementations and the template Lido uses upgradeable proxy contracts as storage for the state. Each proxy contract points to an implementation contract providing the code that reads and mutates the state of the proxy. Implementation contracts can be upgraded via DAO voting. Implementations are immutable, they are only allowed to modify the caller's (i.e. proxy) contract state. In order to setup the protocol, one needs to deploy initial versions of the implementations. Some popular multisig vaults, e.g. Gnosis Safe, don't support deploying new contracts so this has to be done from a usual address. Part of the protocol deployment logic is incorporated in a contract called `LidoTemplate.sol`, which also needs to be deployed prior to running further steps. ### Prepare the network state file The deployment scripts use a JSON file named `deployed-.json` to read the initial environment and protocol configuration and to store data that needs to be persisted between deployment steps. If a deployment step requires anything except RPC endpoint and ETH accounts, then it needs to be specified in the network state file. These files are meant to be added under the source control. If some data is missing from the file, the deployment step will fail with an error saying what's exactly missing. The first step requires the following values: - `networkId` id of the network - `ensAddress` ENS registry address - `daoFactoryAddress` Aragon `DAOFactory` contract address - `apmRegistryFactoryAddress` Aragon `APMRegistryFactory` address - `miniMeTokenFactoryAddress` Aragon `MiniMeTokenFactory` address - `aragonIDAddress` aragonID `FIFSResolvingRegistrar` address - `multisigAddress` the address of the multisig contract that will be used in the next steps to perform the further deployment For example, a network state file for `mainnet` will be named `deployed-mainnet.json` and will initially look like this: ```json { "networkId": 1, "ensAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", "daoFactoryAddress": "0x7378ad1ba8f3c8e64bbb2a04473edd35846360f1", "apmRegistryFactoryAddress": "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A", "miniMeTokenFactoryAddress": "0x909d05f384d0663ed4be59863815ab43b4f347ec", "aragonIDAddress": "0x546aa2eae2514494eeadb7bbb35243348983c59d", "multisigAddress": "YOUR_MULTISIG_CONTRACT_ADDRESS" } ``` Please note that setting `multisigAddress` correctly is very important: this address will own the deployed template contract, and so only this address will be able to perform the deployment steps starting from Lido APM deploy (step 5). ### Generate transaction data files After preparing the values in network state file, generate a set of JSON files with transaction data: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/01-deploy-lido-template-and-bases.js ==================== Network ID: 1 Reading network state from /Users/me/lido-dao/deployed-mainnet.json... ==================== Saving deploy TX data for LidoTemplate to tx-01-1-deploy-template.json Saving deploy TX data for Lido to tx-01-2-deploy-lido-base.json Saving deploy TX data for LidoOracle to tx-01-3-deploy-oracle-base.json Saving deploy TX data for NodeOperatorsRegistry to tx-01-4-deploy-nops-base.json ==================== Before continuing the deployment, please send all contract creation transactions that you can find in the files listed above. You may use a multisig address if it supports deploying new contract instances. ==================== Writing network state to /Users/me/lido-dao/deployed-mainnet.json... All done! ``` ### Send the transactions You can use the `tx` helper for sending the transactions from files. It supports the following flags: - `--from` the sender address - `--file` the TX file which may contain the following fields: `to`, `value`, `data`, `gas`, `from` - `--gas-price` gas price in wei (optional) - `--nonce` sender nonce (optional) - `--wait` the number of seconds to wait before sending the tx (optional, default 5) Run the following to deploy the implementations and the template: ```text $ yarn hardhat --network mainnet tx --from $DEPLOYER --file tx-01-1-deploy-template.json $ yarn hardhat --network mainnet tx --from $DEPLOYER --file tx-01-2-deploy-lido-base.json $ yarn hardhat --network mainnet tx --from $DEPLOYER --file tx-01-3-deploy-oracle-base.json $ yarn hardhat --network mainnet tx --from $DEPLOYER --file tx-01-4-deploy-nops-base.json ``` You're not required to use this helper to send the transactions defined in the generated files; it's there for the convenience only. > This step is an exception from the "sequential transactions" rule: you can send all four > transactions in parallel from different addresses. ### Update the network state file After all four transactions are included in the blockchain, update the network state file with the following values: - `daoTemplateDeployTx` hash of the TX sent from the `tx-01-1-deploy-template.json` file - `lidoBaseDeployTx` hash of the TX sent from the `tx-01-2-deploy-lido-base.json` file - `oracleBaseDeployTx` hash of the TX sent from the `tx-01-3-deploy-oracle-base.json` file - `nodeOperatorsRegistryBaseDeployTx` hash of the TX sent from the `tx-01-4-deploy-nops-base.json` file ## 2. Verifying the deployed contracts Run the following: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/02-obtain-deployed-instances.js ``` This step will verify the deployed contracts and add the following fields to the network state file: - `daoTemplateAddress` address of the `LidoTemplate` contract - `app:lido.baseAddress` address of the `Lido` implementation contract - `app:oracle.baseAddress` address of the `LidoOracle` implementation contract - `app:node-operators-registry.baseAddress` address of the `NodeOperatorsRegistry` implementation contract ## 3. Register a ENS domain for Lido APM This ENS domain is needed for Aragon Package Manager (APM) instance that the protocol will use for the upgrade mechanics. Prior to running the step, add the following keys to the network state file: - `lidoApmEnsName` the second-level ENS domain that APM will use to register packages - `lidoApmEnsRegDurationSec` the domain lease duration in seconds Then, run: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/03-register-ens-domain.js ... ==================== Saving data for commit transaction to tx-02-1-commit-ens-registration.json (projected gas usage is 53667) Saving data for register transaction to tx-02-2-make-ens-registration.json ==================== Before continuing the deployment, please send all transactions listed above. Make sure to send the second transaction at least 60 seconds after the first one is included in a block, but no more than 86400 seconds after that. ==================== ``` The step will generate two transaction files. You'll need to send these transactions one after another, waiting no less than one minute between them: ```text $ yarn hardhat --network mainnet tx --from $DEPLOYER --file tx-02-1-commit-ens-registration.json $ sleep 60 $ yarn hardhat --network mainnet tx --from $DEPLOYER --file tx-02-2-make-ens-registration.json ``` ## 4. Deploy Lido frontend apps The Lido DAO includes frontend apps for DAO governance and protocol management. They are deployed to IPFS, so you'll need to specify `ipfsAPI` key in the network state file pointing to an IPFS client API endpoint, e.g. `"ipfsAPI": "http://localhost:5001/api/v0"`. Then, run the following: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/04-publish-app-frontends.js ``` Make sure that either the IPFS node you're using is going to be permanently up and publicly available, or that you pin the uploaded content to some other permanent public node. This step will add `ipfsCid` and `contentURI` subkeys for all three Lido apps (`app:lido`, `app:oracle`, `app:node-operators-registry`) in the network state file. The first key is the IPFS identifier for the root entry of the app frontend, and `contentURI` is the same key encoded to an Aragon-specific format. ## 5. Deploy Lido APM Run the following: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/05-deploy-apm.js ... ==================== Parent domain: eth 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae Subdomain label: lidopm-pre 0x1353eb779a45ed66bdb49e45e006df81a69d9f73067e846003b5bb00984191d4 ==================== Saving data for APM deploy transaction to tx-03-deploy-apm.json (projected gas usage is 6263517) ==================== ``` The step will generate a transaction file; you'll need to send this transaction from the contract at `multisigAddress`. After the transaction is included in a block, move to the next step. ### Using Gnosis Safe If you're using Gnosis Safe, this can be done by choosing `New Transaction > Contract Interaction` and enabling the `Use custom data (hex encoded)` option in the popped dialog. Then, copy the contents of the `to` key from the transaction JSON file to the `Recipient*` field, the contents of the `value` field to the `Value*` field (enter `0` if there's no `value` key in the transaction JSON), and the contents of the `data` field to the `Data (hex encoded)*` field. Make sure to check the gas limit of the transaction: Gnosis Safe frequently sets it too low. As a rule of thumb, set it to the value of the `gas` key in the transaction JSON file plus `1500000` (the additional gas is used to handle multisig logic). ## 6. Check the deployed APM Run the following: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/06-obtain-deployed-apm.js ``` Make sure that it finishes without errors and move to the next step. The following field will be added to the network state file: - `lidoApmAddress` the address of the Lido APM controlling `lidoApmEnsName` ENS domain. ## 7. Create application APM repositories Run the following: ```text yarn hardhat --network mainnet run ./scripts/multisig/07-create-app-repos.js ... ==================== Saving data for createRepos transaction to tx-04-create-app-repos.json (projected gas usage is 7160587) ==================== ``` The step will generate a transaction file; you'll need to send this transaction from the contract at `multisigAddress`. After the transaction is included in a block, move to the next step. ## 8. Deploy DAO and its governance token This step will deploy the instances of the DAO and governance token. You'll need to add a field called `daoInitialSettings` to the network state file prior to running the step: ```js // ... "daoInitialSettings": { // Governance token name/symbol; cannot be changed post-deploy "token": { "name": "Lido DAO Token", "symbol": "LDO" }, // Beacon chain spec; can be changed via DAO voting "beaconSpec": { "depositContractAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", "slotsPerEpoch": 32, "secondsPerSlot": 12, "genesisTime": 1606824023, "epochsPerFrame": 225 // Lido oracles report once per epochsPerFrame epochs }, // DAO voting configuration (Aragon Voting app) "voting": { "minSupportRequired": "500000000000000000", // 1e18 === 100% "minAcceptanceQuorum": "50000000000000000", // 1e18 === 100% "voteDuration": 172800 // in seconds }, // Protocol fee configuration; can be changed via DAO voting "fee": { "totalPercent": 10, "treasuryPercent": 0, "insurancePercent": 50, "nodeOperatorsPercent": 50 } } // ... ``` Then, run the following: ```text $ yarn hardhat --network mainnet run ./scripts/multisig/08-deploy-dao.js ... Saving data for newDAO transaction to tx-05-deploy-dao.json (projected gas usage is 7118882) ``` Send the generated transaction from the contract at `multisigAddress`. After the transaction is included in a block, move to the next step. ## 9. Check the deployed DAO Run the following: ```text yarn hardhat --network mainnet run ./scripts/multisig/09-obtain-deployed-dao.js ``` Make sure that it finishes without errors and move to the next step. The following fields will be added to the network state file: - `daoAddress` the address of the DAO instance; - `daoTokenAddress` the address of the DAO governance token; - `proxyAddress` keys under `app:*` keys: addresses of the app instances. ## 10. Issue DAO governance tokens Add the `vestingParams` key to the network state file containing the following: ```js // ... "vestingParams": { // unvested tokens will be held on the DAO Agent app "unvestedTokensAmount": "10000000000000000000000", // token holder addresses and their respective amounts "holders": { "0xaabbcc0000000000000000000000000000000000": "100000000000000000000", // ... }, // Vesting start date "start": 1608213253, // Vesting cliff date "cliff": 1608213253, // Vesting end date "end": 1608501253, // Whether vestings should be revokable by the DAO "revokable": false // See https://github.com/aragon/aragon-apps/blob/master/apps/token-manager/contracts/TokenManager.sol } // ... ``` Then, run the following: ```text yarn hardhat --network mainnet run ./scripts/multisig/10-issue-tokens.js ... ==================== Total batches: 2 Saving data for issueTokens (batch 1) transaction to tx-06-1-issue-tokens.json (projected gas usage is 6478755) Saving data for issueTokens (batch 2) transaction to tx-06-2-issue-tokens.json ``` Send the generated transactions sequentially from the contract at `multisigAddress`, waiting until the first one is included in a block before sending the second one. After the second transaction is included in a block, move to the next step. ## 11. Finalize the DAO Add the `daoAragonId` key to the network state file, setting it to a name that the DAO will be registered by in aragonID, i.e. `.aragonid.eth` will resolve to the `daoAddress`. Run the following: ```text yarn hardhat --network mainnet run ./scripts/multisig/11-finalize-dao.js ... ==================== Saving data for finalizeDAO transaction to tx-07-finalize-dao.json (projected gas usage is 5011582) ``` Send the generated transaction from the contract at `multisigAddress`. After the transaction is included in a block, move to the next step. ## 12. Perform the final checks At this point, the DAO is fully deployed. Run the following to verify the correctness of the configuration and permissions setup: ```text yarn hardhat --network mainnet run ./scripts/multisig/12-check-dao.js ``` If there's some error, it will be printed and further checks will be cancelled. This step only requires the following fields to be defined in the network state file: - `ensAddress` - `lidoApmEnsName` - `daoAragonId` - `vestingParams` - `daoInitialSettings` - `daoTemplateAddress` --- # Shadow Owner Detection in Safe Guide based on [**bartek.eth**](https://x.com/bkiepuszewski) X threads: [1](https://x.com/bkiepuszewski/status/1722287321997779427), [2](https://x.com/bkiepuszewski/status/1722914827113312584), [3](https://x.com/bkiepuszewski/status/1727437292309192949) ### Introduction In Safe multisig wallets, besides the "public" owners visible in the UI, there can be "shadow" owners. These shadow owners are authorized to execute transactions but are not listed in the `getOwners()` method, making them invisible in the Safe UI. This guide will help you determine if there is suspicion of shadow owners in the multisig. ### Understanding Owner Structure Safe maintains an [address] -> [address] mapping where each owner's address points to the next owner's address or a 'sentinel' (a dummy address indicating the end of owners list). This setup allows additional storage where owner addresses pointing to non-zero values might not be visible in the Safe UI. Owners added through this additional mechanism do not appear in the standard `getOwners()` list. ![shadow 1](/img/shadow-signers/1.png) Pic. source: [**bartek.eth**](https://x.com/bkiepuszewski) ### Adding a Shadow Owner Safe allows additional delegateCalls during: - `setup()` call - `executeTransaction()` call - `executeTransactionFromModule()` call Any of these calls can modify arbitrary storage slots and add a shadow owner. The transaction might call another contract that adds a new owner to the multisig. ![shadow 2](/img/shadow-signers/2.png) Pic. source: [**bartek.eth**](https://x.com/bkiepuszewski) Although the Safe UI will warn about unexpected delegate calls, it is not always malicious but should be viewed with suspicion. ![shadow 3](/img/shadow-signers/3.png) Pic. source: [**bartek.eth**](https://x.com/bkiepuszewski) ### Detecting Shadow Owners You cannot see shadow owners in the UI or directly determine their addresses. However, you can check for "dirty" storage, which might indicate the presence of shadow owners. Here's how to do it: #### 1 **Access [Token Flow Database](https://tokenflow.live/)** A login is required to access the database. #### 2 **Login and Query** - Use this query: [`https://app.tokenflow.live/studio/editor/66c451806875f9a936d548c9`](https://app.tokenflow.live/studio/editor/66c451806875f9a936d548c9). This query is to check Safe multisig on Ethereum. - Enter the address of the Safe in the query parameter - Click "RUN" ![shadow 4](/img/shadow-signers/4.png) **NB**: The query might take a while to run. #### 3 **Analyze Results** Smart contracts use storage slots written according to a specific layout. Let's see Safe storage layout: ![shadow 5](/img/shadow-signers/5.jpg) Pic. source: [**bartek.eth**](https://x.com/bkiepuszewski) When you analyze the results of the query: ![shadow 6](/img/shadow-signers/6.png) - The first two columns (`MEM_HASH`, `RAW_LOCATION`) show the "raw" storage slots as seen by the EVM . - The `LOCATION` column is a decoded storage slot derived from the Safe smart contract storage layout. - Look for storage locations that could not be decoded using the known storage layout. Such storage must have been set directly by an `STORE` assembly call, indicating "dirty" storage. Example below: ![shadow 7](/img/shadow-signers/7.png) Pic. source: [**bartek.eth**](https://x.com/bkiepuszewski) - To check the value of such โ€œdirtyโ€ storage you can use https://storage-slots.swiss-knife.xyz/, select โ€œcustomโ€, enter the multisig address and the storage slot in to the form and hit query button: ![shadow 8](/img/shadow-signers/8.png) Alternatively if you have [brownie](https://github.com/eth-brownie/brownie) installed, enter following commands in the console: ```sh brownie console from brownie import web3 web3.eth.get_storage_at("{multisig address}", "{storage slot}") ``` Results should look like that: ![shadow 9](/img/shadow-signers/9.png) Or if you have Foundry installed, you can use the following command: ```sh cast storage {multisig address} {storage slot} ``` :::note ๐Ÿ’ก In Safe `0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5` storage slot by default is used to store [fallback handler address](https://etherscan.io/address/0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4), you can read more about it [here](https://help.safe.global/en/articles/40838-what-is-a-fallback-handler-and-how-does-it-relate-to-safe) and find this address in the [official documentation](https://docs.safe.global/advanced/smart-account-supported-networks/v1.3.0#ethereum-mainnet). ::: ### Next Steps if a Suspicious Multisig is Found If your analysis reveals suspicious activity within a multisig, follow these steps to ensure security: **For New Multisigs**: 1. **Do not accept the multisig**. If the multisig shows signs of suspicious activity or dirty storage, do not proceed with using it 2. **Investigate**. Conduct a thorough investigation into the source of the multisig and those who proposed its use **For Existing Multisigs**: 1. **Migrate to a new multisig**. If a currently used multisig is found to be suspicious, create a new multisig with the same set of public signers and transfer all assets to it 2. **Identify malicious actors**. Investigate and identify any individuals responsible for the malicious transactions By following these steps, contributors can maintain security, ensuring that only authorized owners have control. --- # Multisig Signer Guide Lido DAO uses multisig wallet for different ops for flexibility and fast reaction times. Here's a list of general tips & rules around being the signer: 1. Use hardware wallet & back up the seed phrase. 2. Upon joining to any multisig make sure to verify the address according [to the guide](/guides/address-ownership-guide/). 3. Check transactions you see on the multisig. If it's unclear to what the transaction should do and why โ€” don't sign it and ask for explanation. 4. Every transaction should have "how to check" guide. 1. Addresses & sums in question should be verifiable through third party sources (message from your fellow multisig co-signer doesn't cut it). 2. Technical checks, if those are required (i.e.for smart contract interaction), should rely on ready-made third party software and not custom scripts & UIs. 5. While requesting the signatures tell explicitly whether the transaction can be executed right away or would need to wait for something. 6. Communicate once you've checked and signed the transaction ("checked, signed, X more required"). 7. If the transaction can be executed right away โ€” the last signer does it. If they can't do for any reason โ€” communicate it. 8. If you're executing already signed transaction โ€” make sure to check it as if you were signing it. --- # Oracle Operator Manual This document is intended for those who wish to participate in the Lido protocol as entity that runs - an entity who runs a daemons synchronizing state from Beacon Layer to Execution Layer of the protocol. Due to the lack of native communication between these two networks, Lido employs a network of oracles to synchronize the system at regular intervals. ## TL;DR 1. Generate an Ethereum address. 2. Launch and sync an [archive](https://ethereum.org/en/developers/docs/nodes-and-clients/#archive-node) (archive data for at least 2 weeks) Execution Layer node with JSON-RPC endpoint enabled. 3. Launch and sync an [archive](https://ethereum.org/en/developers/docs/nodes-and-clients/#archive-node) Consensus Layer node with API endpoint enabled. 4. Launch and sync a [Keys API Service](https://github.com/lidofinance/lido-keys-api). 5. Launch the **accounting**, **ejector**, and **csm** modules of the Oracle. 6. [**Optional**] Add alerts to Oracle's Prometheus metrics. 7. In case of mainnet, share your address and intention to join the Oracle set with the public. You need to publish it on Twitter and also write a message with a Twitter link under the Onboarding post on [the Research forum](https://research.lido.fi/). You need to publish it on Twitter and also write a message with a twitter link under the Onboarding post on [the Research forum](https://research.lido.fi/). 8. Propose your Oracle's Ethereum address to the Lido team to vote on adding your address to the Oracle Members. 9. After the [LIP-37](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) vote, the seat is held by a `DelegationContract` instead of an EOA: deploy it and configure the daemon as described in the [EDF Operator Guide](/guides/edf/edf-operator-guide). ## Intro The Lido Oracle mechanism comprises three main components. The first component is the Oracle smart-contract suite, which receives update reports from the oracles and passes them on to the Lido contract to execute the necessary actions based on the reported changes. The second component is the off-chain oracle daemon, run by each oracle node and responsible for monitoring the protocol state and generating update reports. The third component is the network of computer nodes that run by oracle member, which collectively provide the necessary information to the Oracle smart contract to calculate the new state of the protocol. Based on the update reports received from the oracles, the Lido smart contract performs state transitions such as updating user balances, processing withdrawal requests, and distributing rewards to node operators. Thus, the Lido Oracle mechanism acts as a synchronization device that bridges the protocol across the execution and consensus layers. It ensures that the protocol is updated in a timely and accurate manner and allows for smooth and efficient operation of the entire Lido system. The two core contracts in the Lido Oracle suite are called [AccountingOracle](/contracts/accounting-oracle/) and [ValidatorsExitBus](/contracts/validators-exit-bus-oracle/). Together, these contracts collect information submitted by oracles about the state of validators and their balances, the amount of funds accumulated on protocol vaults, the number of withdrawal requests the protocol is able to process, and the validators are expected to be voluntary exited to finalize more withdrawal requests. This information is then used for these crucial processes: - rebasing user balances, - distributing node operator rewards, - processing withdrawal requests, - making decision which validators should initiate voluntary exit, - distributing stake, - putting the protocol into the bunker mode. ## Oracle phases In order to send the report data by the oracle operator to both `AccountingOracle` and `ValidatorsExitBusOracle`, it is necessary that: - this operator participates in the oracle committee, and - a consensus for the corresponding report must be reached A process of sending the report data can be divided into 3 major stages: ### Phase 1. Submitting a report hash and reaching consensus At the first stage, the oracles operators collect a report for a certain `refSlot` and send the hash to the `HashConsensus` contract. The diagram below shows: `ReportProcessor` - `AccountingOracle` or `ValidatorsExitBusOracle` contract. `HashConsensus` - a contract which manages oracle members committee and allows the members to reach consensus on the particular data hash for each reporting frame. You can read more about HashConsensus [here](/contracts/hash-consensus/). ```mermaid graph LR; O1[Oracle 1] --submitReport--> HashConsensus; O2[Oracle N] --submitReport--> HashConsensus; subgraph oracles O1 O2 end HashConsensus-->|event ReportReceived| B{Consensus reached?} B -->|Yes| cns[/submitReportForProcessing/]-->ReportProcessor B -->|No| prev[/check prevConsensusLost/] ``` ### Phase 2. Submitting a report data When the consensus is reached, one of the oracles operators submits report data and triggers the core protocol state update (including the token rebase, distribution of node operator rewards, finalization of withdrawal requests, and deciding whether to go in the bunker mode) or emits `ValidatorExitRequest` events to inform node operators about new voluntary exit requests needed to perform. ```mermaid graph LR; O1[Oracle 1] --submitReportData--> ReportContract; ReportContract --> B{Consensus reached?} B-->|Yes| handleConsensusReportData ``` ### Phase 3. Submitting a report extra data This step is required for `AccountingOracle`, involving reward distribution for staking modules on this phase. ```mermaid graph LR; O1[Oracle 1] -->B{extra data?}; B-->Yes B-->No Yes -->|submitReportExtraDataList| AccountingOracle No -->|submitReportExtraDataEmpty| AccountingOracle subgraph oracle O1 B Yes No end ``` ## Committee membership The current Oracle set consists of 9 participants with a quorum of 5. This means that report finalization can only occur when there are 5 identical reports from 5 different oracle members. The actual list of Oracle participants list can be fetched from the HashConsensus contract using the [`getMembers`](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288#readContract#F16) method. *Hoodi Oracle participants' addresses can be found [here](https://hoodi.etherscan.io/address/0x32EC59a78abaca3f91527aeB2008925D5AaC1eFC#readContract#F16)* The latest updates can be found in the [Expansion of Lido on Ethereum Oracle set](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836) post. The current members and their addresses are also listed on the [Lido Oracle](/holders/lido-oracle#mainnet-members) page. After the [LIP-37](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746) vote, an Oracle seat is held by the member's `DelegationContract` under the [Execution Delegation Framework (EDF)](/guides/edf/edf-operator-guide), not by an EOA. The member's hot key becomes the delegate of that contract and can be rotated or revoked by the member without a governance vote. See the [EDF Operator Guide](/guides/edf/edf-operator-guide) for the setup. ## Prerequisites ### Execution Client Node To prepare reports, the Oracle might fetch a few months' worth of old events. It also makes historical requests for balance data and simulates reports on historical blocks. This requires an [archive](https://ethereum.org/en/developers/docs/nodes-and-clients/#archive-node) execution node. | Client | Tested | Notes | |-------------------------------------------------|:------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Geth](https://geth.ethereum.org/) | ๐ŸŸข | `--gcmode=archive` `--syncmode=snap` OR `--gcmode=archive` `--syncmode=full` | | [Nethermind](https://nethermind.io/) | ๐Ÿ”ด | Not tested yet | | [Besu](https://besu.hyperledger.org/en/stable/) | ๐Ÿ”ด | Recent changes require FULL sync | | [Erigon](https://github.com/ledgerwatch/erigon) | ๐ŸŸข | Use `--prune=rhtc` `--prune.r.before=324000` `--prune.h.before=324000` `--prune.t.before=256` `--prune.c.before=256` params | | [Reth](https://reth.rs/) | ๐Ÿ”ด | Not tested yet | ### Consensus Client Node To calculate some metrics for bunker mode Oracle needs [archive](https://ethereum.org/en/developers/docs/nodes-and-clients/#archive-node) consensus node. | Client | Tested | Notes | |---------------------------------------------------|:------:|-----------------------------------------------------------------------------------------------------------------------------------------------------| | [Lighthouse](https://lighthouse.sigmaprime.io/) | ๐ŸŸข | Use `--reconstruct-historic-states` param | | [Lodestar](https://nethermind.io/) | ๐Ÿ”ด | Not tested yet | | [Nimbus](https://nimbus.guide/quick-start.html) | ๐Ÿ”ด | Not tested yet | | [Prysm](https://github.com/ledgerwatch/erigon) | ๐ŸŸข | Use `--grpc-max-msg-size=104857600` `--enable-historical-state-representation=true` `--slots-per-archive-point=1024` params | | [Teku](https://docs.teku.consensys.net) | ๐ŸŸข | Use `--data-storage-mode=archive` `--data-storage-archive-frequency=1024` `--reconstruct-historic-states=true` params | | [Grandine](https://grandine.io/) | ๐Ÿ”ด | Not tested yet | ### Keys API Service This is a separate service that uses the Execution Client to fetch all Lido keys. It stores the latest state of Lido keys in a database. [Lido Keys API repository.](https://github.com/lidofinance/lido-keys-api) ## The oracle daemon The Oracle daemon is a Python application that contains the following modules: - Accounting module - Ejector module - CSM module The oracle source code is available at [https://github.com/lidofinance/lido-oracle](https://github.com/lidofinance/lido-oracle). ### Environment variables The oracle daemon requires the following environment variables: **Required** - `EXECUTION_CLIENT_URI` - list of Execution Client uris separated with comma. The second and next uris will be used as fallback. - `CONSENSUS_CLIENT_URI` - list of Consensus Client uris separated with comma. The second and next uris will be used as fallback. - `KEYS_API_URI` - list of Key API client uris separated with comma. The second and next uris will be used as fallback. - `LIDO_LOCATOR_ADDRESS` - Lido Locator smart contract address. Additional variables required by the CSM module: - `CSM_MODULE_ADDRESS` - Community Staking Module address. - `PINATA_JWT` - Pinata IPFS provider JWT token. - `PINATA_DEDICATED_GATEWAY_URL` - Pinata IPFS provider dedicated gateway URL. - `PINATA_DEDICATED_GATEWAY_TOKEN` - Pinata IPFS provider dedicated gateway access token. **Optional** **One of:** - `MEMBER_PRIV_KEY` - Private key of the Oracle member account. - `MEMBER_PRIV_KEY_FILE` - A path to the file contained the private key of the Oracle member account. **EDF (delegation), see the [EDF Operator Guide](/guides/edf/edf-operator-guide#part-2--configure-the-lido-oracle):** - `DELEGATION_CONTRACT_ADDRESS` - The member's `DelegationContract` address. When empty, delegation is off. - `MEMBER_PRIV_KEY_2` / `MEMBER_PRIV_KEY_2_FILE` - The second key, the delegate of the `DelegationContract`. Used together with `MEMBER_PRIV_KEY` during the migration and for key rotation. Full list could be found [here](https://github.com/lidofinance/lido-oracle#env-variables). ### Lido Locator address **Mainnet** **[0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb](https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb)** **Hoodi** **[0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8 ](https://hoodi.etherscan.io/address/0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8 )** ### Running the daemon Startup accounting module ```shell docker run -d --name lido-oracle-accounting \ --env "EXECUTION_CLIENT_URI=$EXECUTION_CLIENT_URI" \ --env "CONSENSUS_CLIENT_URI=$CONSENSUS_CLIENT_URI" \ --env "KEYS_API_URI=$KEYS_API_URI" \ --env "LIDO_LOCATOR_ADDRESS=$LOCATOR_ADDRESS" \ --env "MEMBER_PRIV_KEY=$MEMBER_PRIV_KEY" \ lidofinance/oracle@ accounting ``` Startup ejector module ```shell docker run -d --name lido-oracle-ejector \ --env "EXECUTION_CLIENT_URI=$EXECUTION_CLIENT_URI" \ --env "CONSENSUS_CLIENT_URI=$CONSENSUS_CLIENT_URI" \ --env "KEYS_API_URI=$KEYS_API_URI" \ --env "LIDO_LOCATOR_ADDRESS=$LOCATOR_ADDRESS" \ --env "MEMBER_PRIV_KEY=$MEMBER_PRIV_KEY" \ lidofinance/oracle@ ejector ``` Startup CSM module ```shell docker run -d --name lido-oracle-csm \ --env "EXECUTION_CLIENT_URI=$EXECUTION_CLIENT_URI" \ --env "CONSENSUS_CLIENT_URI=$CONSENSUS_CLIENT_URI" \ --env "KEYS_API_URI=$KEYS_API_URI" \ --env "LIDO_LOCATOR_ADDRESS=$LOCATOR_ADDRESS" \ --env "MEMBER_PRIV_KEY=$MEMBER_PRIV_KEY" \ --env "CSM_MODULE_ADDRESS=$CSM_MODULE_ADDRESS" \ --env "PINATA_JWT=$PINATA_JWT" \ --env "PINATA_DEDICATED_GATEWAY_URL=$PINATA_DEDICATED_GATEWAY_URL" \ --env "PINATA_DEDICATED_GATEWAY_TOKEN=$PINATA_DEDICATED_GATEWAY_TOKEN" \ lidofinance/oracle@ csm ``` #### Persistent cache of CSM module CSM module of the Oracle uses a cache to store per-epoch data of network-wide validator performance. It takes a significant amount of time to collect the data from scratch. That's why it's encouraged to set up a persistent cache location for the oracle outside of a Docker container in order to keep the cache in case of container destruction for maintenance purposes. The `CACHE_PATH` environment variable sets the path to a directory where the oracle will store its cache. Run the container with the additional arguments: ```shell docker run -d --name lido-oracle-csm \ "...required_variables_from_the_example_above" \ --env "CACHE_PATH=/app/cache" \ --volume "/var/lib/lido_csm_cache/:/app/cache" lidofinance/oracle@ csm ``` Make sure the correct permissions are set up for the mounted directory. The UID of the oracle process in the image provided by Lido is `33`, and it might require changing the owner of the directory on the host machine: ```shell chown -R 33:33 /path/to/cache/on/host ``` **Latest image hash - [link](/guides/tooling/#oracle)** This will start the oracle in daemon mode. You can also run it in a one-off mode, for example if youโ€™d prefer to trigger oracle execution as a `cron` job. In this case, set the `DAEMON` environment variable to 0. ### Metrics and Alerts How to set up alerts and details about metrics could be found [here](https://github.com/lidofinance/lido-oracle#alerts). --- # Accounting oracle :::info It's advised to read [What is Lido Oracle mechanism](/guides/oracle-operator-manual/#intro) before ::: ## Withdrawal stage As withdrawals on Ethereum are processed asynchronously, the Lido protocol has to have a request-claim process for `stETH` holders. To ensure the requests are processed in the order they are received, the in-protocol FIFO queue is introduced. Here is an overview of the withdrawals handling process: 1. **Request:** To withdraw stETH to ether, one sends the withdrawal request to the [`WithdrawalQueue`](/docs/contracts/withdrawal-queue-erc721.md) contract, locking the `stETH` amount to be withdrawn. 2. **Fulfillment:** The protocol handles the requests one-by-one, in the order of creation. Once the protocol has enough information to calculate the `stETH:ETH` redemption rate of the next request and obtains enough Ether to handle it, the request can be finalized: the required amount of ether is reserved and the locked stETH is burned. 3. **Claim:** The requestor can then claim their ether at any time in the future. The `stETH:ETH` redemption rate for each request is determined at the time of its finalization and is the inverse of the `ETH:stETH` staking rate. :::note It's important to note that the redemption rate at the finalization step may be lower than the rate at the time of the withdrawal request due to slashing or penalties that have been incurred by the protocol. This means that one may receive less Ether for their stETH than they expected when they originally submitted the request. ::: One can put any number of withdrawal requests in the queue. While there is an upper limit on the size of a particular request, there is no effective limit as the requestor can submit multiple withdrawal requests. There's a minimal request size threshold of `100 wei` required as well due to [rounding error issues](https://github.com/lidofinance/lido-dao/issues/442). A withdrawal request could be finalized only when the protocol has enough ether to fulfill it completely. Partial fulfillments are not possible, however, one can accomplish similar behavior by splitting a bigger request into a few smaller ones. For UX reasons, the withdrawal request is transferable being a non-fungible [ERC-721](https://ethereum.org/ru/developers/docs/standards/tokens/erc-721/) compatible token. It is important to note two additional restrictions related to withdrawal requests. Both restrictions serve to mitigate possible attack vectors allowing would-be attackers to effectively lower the protocol's APR and carry fewer penalties/slashing risk than `stETH` holders staying in the protocol. 1. **Withdrawal requests cannot be canceled.** To fulfill a withdrawal request, the Lido protocol potentially has to eject validators. A malicious actor could send a withdrawal request to the queue, wait until the protocol sends ejection requests to the corresponding Node Operators, and cancel the request after that. By repeating this process, the attacker could effectively lower the protocol APR by forcing Lido validators to spend time in the activation queue without accruing rewards. If the withdrawal request can't be canceled, there vulnerability is mitigated. As noted above, making the position in the withdrawal queue transferable can provide a "fast exit path" for regular stakers via external secondary markets. 2. **The redemption rate at which a request is fulfilled cannot be better than the redemption rate on the request creation.** Otherwise, thereโ€™s an incentive to always keep the stETH in the queue, depositing ether back once itโ€™s redeemable, as this allows to carry lower staking risks without losing rewards. This would also allow a malicious actor to effectively lower the protocol APR. To avoid this, the penalties leading to a negative rebase are accounted for and socialized evenly between stETH holders and withdrawers. Positive rebases could still affect requests in the queue, but only to the point where rebases compensate for previously accrued penalties and don't push the redemption rate higher than it was at the moment of the withdrawal request's creation. ### Request finalization On each report, the oracle decides how many requests to finalize and at what rate. Requests are finalized in the order in which they were created by moving the cursor to the last finalized request. Oracle must take two things into account: 1. Available ether and redemption rate (also called as 'share rate') 2. Safe requests finalization border #### Available ether and share rate The Oracle report has two parts: the report of the Lido validators' balances on the Consensus Layer and the finalization of requests in the [`WithdrawalQueue`](/docs/contracts/withdrawal-queue-erc721.md). The finalization of requests requires data from the first part of the report. Therefore, to calculate this part the oracle report is simulated by calling `Accounting.simulateOracleReport`, getting share rate and amount of ether that can be withdrawn from [Withdrawal](/docs/contracts/withdrawal-vault.md) and [Execution Layer Rewards](/docs/contracts/lido-execution-layer-rewards-vault.md) Vaults taking into account the limits. The structure of the data for simulation (`Accounting.ReportValues`): - `timestamp` - the moment of the oracle report calculation, calculated as `timestamp = genesis_time + ref_slot * seconds_per_slot`; - `timeElapsed` - seconds elapsed since the previous reported ref slot and the simulated one - `clValidatorsBalance` - sum of all Lido active validators' balances on the Ethereum Consensus Layer (excluding pending deposits), in wei - `clPendingBalance` - sum of pending deposits attributed to Lido keys on the Consensus Layer (deposited on the Execution Layer, not yet activated), in wei - `withdrawalVaultBalance` - withdrawal vault balance on the Ethereum Execution Layer for the reported block - `elRewardsVaultBalance` - elRewards vault balance on the Ethereum Execution Layer for reported block. Set to "**0**" if try to simulate report in bunker mode - `sharesRequestedToBurn` - gets from `Burner.getSharesRequestedToBurn()` - `withdrawalFinalizationBatches` - Set to "**[]**" - `simulatedShareRate` - share rate that was simulated by oracle when the report data created (`1e27` precision). Set to "**0**" This data is provided to make the call to `Accounting.simulateOracleReport()` and the following retrieved values are gathered: `post_total_pooled_ether` and `post_total_shares`. Therefore, `share_rate` for the withdrawal request finalization can be calculated as follows: ```! share_rate = post_total_pooled_ether * 10**27 // post_total_shares ``` #### Safe requests finalization border Considering withdrawals, the Lido protocol can be in two states: Turbo and Bunker modes. The turbo mode is a usual state when requests are finalized as fast as possible, while the bunker mode assumes a more sophisticated requests finalization and activated if it's necessary to socialize the penalties and losses. More details in the [Withdrawals Landscape](https://hackmd.io/@lido/SyaJQsZoj) doc. **Turbo mode**: there is only a single safe requests finalization border that does not allow to finalize requests created close to the reference slot to which the oracle report is performed. - New requests border (~2 hours by default) **Bunker mode**: there are two additional constraints. The protocol takes into account the impact of negative factors that occurred in a certain period and finalizes requests on which the negative effects have already been socialized. The safe request finalization border is considered to be the earliest of the following: - New requests border - Associated slashing border - Negative rebase border Before examining each border, some notations needed for introduction that are used in the graphs below: ![Safe border 1](../../../static/img/oracle-spec/safe-border-1.png) ##### New requests border The border is a constant interval near the reference epoch in which no requests can be finalized: ![Safe border 2](../../../static/img/oracle-spec/safe-border-2.png) So can be calculated as: `ref_epoch - finalization_default_shift` , where: ![Safe border 3](../../../static/img/oracle-spec/safe-border-3.png) And `SLOTS_PER_EPOCH = 32`, `SECONDS_PER_SLOT = 12`, `request_timestamp_margin` - gets from `OracleReportSanityChecker.oracleReportLimits()`. ##### Associated slashing border The border represents the latest epoch before the reference slot before which there are no incompleted associated slashings. ![Safe border 4](../../../static/img/oracle-spec/safe-border-4.png) In the image above there are 4 slashings on the timeline that start with `slashed_epoch` and end with `withdrawable_epoch` and some points in time: a withdrawal request and reference epoch relationship of the slashings with which to be analyzed. ###### Completed non-associated ![Safe border 5](../../../static/img/oracle-spec/safe-border-5.png) The slashing is non-associated with the withdrawal request since it started and ended before the request was created. It's completed since `withdrawable_epoch` is before `reference_epoch`. ###### Completed associated ![Safe border 6](../../../static/img/oracle-spec/safe-border-6.png) The slashing is associated with withdrawal request, since the request is in its boundaries. In this case the slashing is completed since `withdrawable_epoch` is before `reference_epoch`, so all possible impact from it is accounted. This slashing should not block the finalization of this request. ###### Incompleted associated ![Safe border 7](../../../static/img/oracle-spec/safe-border-7.png) Slashing is associated with the withdrawal request and is still going on. The impact from it is still incomplete, so such a request cannot be finalized. ###### Incompleted non-associated ![Safe border 8](../../../static/img/oracle-spec/safe-border-8.png) Incompleted but non-associated slashing do not block finalization of the request. The impact of it is still incomplete, nevertheless users are allowed to redeem. ###### Computation of the border ![Safe border 9](../../../static/img/oracle-spec/safe-border-9.png) The border is calculated as the earliest `slashed_epoch` among all incompleted slashings at the point of `reference_epoch` rounded to the start of the closest oracle report frame minus `finalization_default_shift`. [Detailed research of associated slashings](https://hackmd.io/@lido/r1Qkkiv3j) ##### Negative rebase border Bunker mode can be enabled by a negative rebase in case of mass validator penalties. In this case the border is considered the reference slot of the previous oracle report from the moment the Bunker mode was activated - `finalization_default_shift`. ![Safe border 10](../../../static/img/oracle-spec/safe-border-10.png) ![Safe border 11](../../../static/img/oracle-spec/safe-border-11.png) This border has a maximum length equal to two times the governance reaction time (where governance reaction time is 120 hours). ##### Border union The safe border is chosen depending on the protocol mode and is always the longest of all. ![Safe border 12](../../../static/img/oracle-spec/safe-border-12.png) ```python def get_safe_border_epoch(ref_epoch): is_bunker = detect_bunker_mode() if not is_bunker: return get_default_requests_border_epoch() negative_rebase_border_epoch = get_negative_rebase_border_epoch() associated_slashings_border_epoch = get_associated_slashings_border_epoch() return min( negative_rebase_border_epoch, associated_slashings_border_epoch, ) ``` #### Finalization With the amount of available ETH, share rate and safe border, the oracle calls `WithdrawalQueue.calculateFinalizationBatches` method to get withdrawal finalization batches. The value of a request after finalization can be: - `nominal` (when the amount of eth locked for this request are equal to the request's stETH) - `discounted` (when the amount of eth will be lower, because the protocol share rate dropped before request is finalized, so it will be equal to `request's shares` \* `protocol share rate`) **Batches** - array of ending request id. Each batch consist of the requests that all have the share rate below the `_maxShareRate` or above it (nominal or discounted). For example, below an example how 14 requests with different share rates will be split into 5 batches by: ``` | | โ€ข โ€ข | โ€ข โ€ข โ€ข โ€ข โ€ข |----------------------โ€ข------ _maxShareRate | โ€ข โ€ข โ€ข โ€ข โ€ข | โ€ข +-------------------------------> requestId | 1st| 2nd |3| 4th | 5th | ``` so: ``` batches = [2, 6 ,7, 10, 14] ``` To start calculation oracle should pass next variables to `WithdrawalQueue.calculateFinalizationBatches` method: - `maxShareRate` - calculated on previous step as simulatedShareRate, share rate that was simulated by oracle when the report data created - `maxTimestamp` - max timestamp of the request that can be finalized - `maxRequestsPerCall` - max request number that can be processed by the call. Better to be max possible number for EL node to handle before hitting `out of gas`. More this number is less calls it will require to calculate the result - `BatchesCalculationState` - structure that accumulates the state across multiple invocations to overcome gas limits. ```solidity struct BatchesCalculationState { /// @notice amount of ether available in the protocol that can be used to finalize withdrawal requests /// Will decrease on each invocation and will be equal to the remainder when calculation is finished /// Should be set before the first invocation uint256 remainingEthBudget; /// @notice flag that is `true` if returned state is final and `false` if more invocations required bool finished; /// @notice static array to store all the batches ending request id uint256[MAX_BATCHES_LENGTH] batches; /// @notice length of the filled part of `batches` array uint256 batchesLength; } ``` To start batch calculation oracle should pass `state.remainingEthBudget` and `state.finished == false` and then invoke the function `calculateFinalizationBatches` again with returned `state` until it returns a state with `finished` flag set. ### Bunker mode The withdrawals mode a mechanism to protect users who are withdrawing during rare but potentially adverse network conditions, such as mass slashing. The proposed mechanism includes a โ€œturbo modeโ€ for normal operation or low to moderate impact events and a โ€œbunker modeโ€ for significant impact events, which pauses withdrawal requests until the negative consequences are resolved. The solution aims to prevent sophisticated users from exiting earlier in anticipation of a dramatic network- or protocol-wide event. The โ€œturbo/bunker modeโ€ aims to create a situation where users who remain in the staking pool, users who exit within the frame, and users who exit within the nearest frames are in nearly the same conditions. The โ€œbunker modeโ€ should activate when there is a negative consensus layer rebase or when one is expected to happen in the future, as it would break the balance between users exiting now and those who remain in the staking pool or exit later. Several scenarios should be taken into consideration as they may cause a negative CL rebase, including mass slashing events, Lido validators being offline for several hours/days, and non-Lido validators being offline. The โ€œbunker modeโ€ is entered in situations such as new or ongoing mass slashing that can cause a negative CL rebase during the slashing resolution period, negative CL rebase in the current frame, and lower than expected CL rebase in the current frame and a negative CL rebase in the end of the frame. #### Bunker mode activation The bunker mode is activated when a negative CL rebase (a decrease in the total amount of staked tokens) is detected in the current frame or is anticipated in the future. CL rebase is used as an indicator of validators' performance, as it provides a better estimate than MEV received during the frame. The conditions for triggering the "bunker mode" are divided into three categories. #### Condition 1. New or ongoing mass slashing that can cause a negative CL rebase The first condition is when there is a new or ongoing mass slashing that may cause a negative CL rebase. The mode is set up when there are as many slashed validators as can cause a negative CL rebase. The protocol switches back to "turbo mode" once the current and future possible penalties from the Lido slashing cannot cause a negative CL rebase. #### Condition 2. Negative CL rebase in the current frame The second condition is when a negative CL rebase is detected in the current frame. The "bunker mode" is activated, and there is a limit on the maximum delay for withdrawal requests finalization that is set to 2 \* gov_reaction_time (~10 days) if there are no associated slashings. #### Condition 3. Lower than expected CL rebase in the current frame and a negative CL rebase at the end of the frame The third condition is when there is a lower-than-expected CL rebase in the current frame and a negative CL rebase at the end of the frame. The "bunker mode" is activated when the Oracle detects this condition. The limit on the maximum delay for withdrawal requests finalization is set to 2 \* gov_reaction_time + 1 (~11 days) if there are no associated slashings. For more details, see [โ€œBunker modeโ€: what it is and how it works](https://docs.google.com/document/d/1NoJ3rbVZ1OJfByjibHPA91Ghqk487tT0djAf6PFu8s8/) --- # Validator exits and penalties The Lido protocol has laid out the policy on validator exit order, performance expectations over time, node operator responsibilities, and monitoring and penalties. The validators' exit should be deterministic and independent to ensure trustlessness, and the current proposed exit order is the "combined approach." Initially, the enforcement mechanisms and service level expectations are mild enough to work out initial kinks without unreasonable penalty, but penalties for non-performance should increase once the processes and mechanisms mature. Node Operators have a duty to exit validators correctly and timely, and the tooling for the semi- or fully-automated processing of validator exit requests includes the Key API Service, Ejector Oracle reports, and Validator Ejector. Node Operators must adhere to the required service levels for validator exits, or they risk being classified as delayed or delinquent. A piece of tooling dubbed the "Monitor Daemon" is served to reconcile signalled validator exit requests with processed exits by the Ethereum Consensus Layer in order to determine if validators have been exited in a timely manner. The results of this monitoring are publicly available in order to ensure the DAO has access to the data it needs to understand the rate, flow, and efficacy of validator exits. Although the process might be largely automated, to account for differences in infrastructure, working hours, and mechanism timings, the below are the required service levels for validator exits that Node Operators must adhere to. If Node Operators are processing signalled validator exit requests as soon as they are available, the shortest possible time for a validator exit request to go from โ€œsignalledโ€ to โ€œprocessedโ€ will be somewhere within the range of a few minutes to an hour. With respect to validator exit performance, each Node Operator may be considered to have one of the below three statuses. * In good standing - validator exit requests are being processed fully, correctly, and timely. * Delayed - validator exit requests are being processed incompletely, incorrectly, or not within the desired time frame. * Delinquent - validator exit requests are being processed incompletely, incorrectly, or not within the maximum acceptable time frame. |Event|Requirement to not be considered Delayed|Requirement not be considered Delinquent| |---|---|---| |Processing of signalled validator exit requests|All signalled requests are processed ASAP (no longer than 1 day)|Some signalled requests are taking longer than 1 but less than 4 days to process| |Escalation of inability to execute signalled validator exit request with reason|ASAP but no longer than 1 day|ASAP but no longer than 4 days In the case that Node Operators are not processing validator exit requests in a timely manner, the below actions shall be taken: If a Node Operator has a status of Delayed, there should be raised an issue in internal communications with the Node Operator and request remediative action. * If a Node Operator has a status of Delinquent, the DAO contributors can raise a formal issue with the Node Operator on the Lido research forum. While a Node Operator has a status of Delinquent: * no new stake will be allocated to the Node Operator (happens automatically); * the daily rewards sent to the Node Operator will be halved (with the remaining half sent towards that dayโ€™s rebase) (happens automatically); * reduced rewards will continue for the duration of a cooldown period long enough to determine whether, immediately after service restoration by the Node Operator, subsequently received validator exit requests are processed in a timely manner. * If a Node Operator has a status of Delayed or Delinquent, the [Validators Exit Bus](/guides/oracle-spec/validator-exit-bus.md) Oracle off-chain module will assume that the Node Operator is unresponsive and re-route new incoming validator exit requests to operators that are not considered delinquent. Due to the re-routing of validator exit requests, the DAO shall consider (via an ad-hoc vote) overriding the total limit of active validators for the relevant Node Operator such that if/when they resume a status of in good standing, they are not benefiting at the expense of Node Operators who took over the processing of the re-routed exits requests. * Once a Delinquent Node Operator has processed all signalled validator exit requests (and thus their number of Delinquent validators in next Accounting Oracle report is updated to 0), they will recommence receiving validator exit requests. Their status shall revert to โ€œIn good standingโ€ after 5 days (i.e. provided any newly received validator exit requests are processed timely). During this 5 day โ€œcooldown periodโ€ they will continue to not receive new stake and receive halved rewards. * In the most egregious of cases (e.g. delinquency for weeks at a time) the DAO may consider an on-chain vote to โ€œstopโ€ the Node Operator which has the effect of setting the fees that they receive to zero (the DAO may consider such a vote at any time). If the Node Operator is unresponsive to the DAOโ€™s requests, then the Node Operator is considered to have been effectively โ€œoff boardedโ€ from the Lido protocol and the DAO should take further steps to formalize the exit of the Node Operator. In the case that a Node Operator cannot, for any reason, exit a validator (e.g. loss of the private key associated with that validator), they are expected to reimburse the protocol participants by supplying the maximum irretrievable balance of the validator (i.e. 32 ETH, since anything over that can be obtained via partial rewards). Doing so renders the validator in question โ€œunrecoverable and reimbursedโ€ and does not count against the Node Operator in terms of assessing its validator exit request status. #### Helpful links * Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)) * [Withdrawals: on Validator Exiting Order](https://research.lido.fi/t/withdrawals-on-validator-exiting-order/3048/1) --- # Validators Exit Bus :::info It's advised to read [What is Lido Oracle mechanism](/guides/oracle-operator-manual/#intro) before ::: [Validators Exit Bus](/contracts/validators-exit-bus-oracle/) is an oracle that ejects Lido validators when the protocol requires additional funds to process user withdrawals. There are two stages of selecting validators for exit: Covering Demand in WQ and Boosted Exits A report calculation consists of 6 key steps: 1. Calculate withdrawals amount to cover with ether. 2. Calculate ether rewards prediction per epoch. 3. Calculate withdrawal epoch for next validator eligible for exit to cover withdrawal requests if needed. 4. Prepare validators exit order queue to fulfill withdrawals. 5. Extend exit list with forced to exit validators (`targetLimitMode` is set to the boosted mode) up to the report limit or until there are no forced requests. 6. Go through the queue until the exited validatorsโ€™ balances cover all withdrawal requests (considering the predicated final exited balance of each validator). :::note Placed exit requests via `ValidatorsExitBusOracle` should be processed timely according to the ratified Lido on Ethereum Validator Exits SNOP 3.0 ([IPFS](https://ipfs.io/ipfs/QmW9kE61zC61PcuikCQRwn82aoTCj9yPuENGNPML9QLkSM), [GitHub](https://github.com/lidofinance/documents-and-policies/blob/main/Lido%20on%20Ethereum%20Standard%20Node%20Operator%20Protocol%20-%20Validator%20Exits.md)). See also the provided [penalties](/guides/oracle-spec/penalties.md) spec. ::: ## Next validator to exit algorithm The algorithm for the validators exiting is based on [the algorithm described on the research forum](https://research.lido.fi/t/withdrawals-on-validator-exiting-order/3048#combined-approach-17). The algorithm is supposed to correct the future number of validators for each Node Operator. Suppose the validators and deposits in-flight of one of the Node Operator are represented in the following form, where validators are sorted by their indexes: ![VEBO 1](../../../static/img/oracle-spec/vebo-1.png) The algorithm assumes that the oldest validators are exited first. Therefore, previously requested validators can be separated to exit by knowing the index of the last requested. ![VEBO 2](../../../static/img/oracle-spec/vebo-2.png) Worth noting, each validator has a status. Some validators may be slashed or be exited without an request from the protocol: ![VEBO 3](../../../static/img/oracle-spec/vebo-3.png) Among all validators the projected ones are the point of interest. They include all active validators and in-flight deposits, but exclude validators whose `exit_epoch != FAR_FUTURE_EPOCH` and those validators that were requested to exit. ![VEBO 4](../../../static/img/oracle-spec/vebo-4.png) A few hours later it might look like the following: ![VEBO 5](../../../static/img/oracle-spec/vebo-5.png) Note that the described algorithm is looking for a validator to exit only among those that can be exited, while using the projected number of validators, which includes non-existent yet validators. It's only weights, so there is no misconception here. The exit order is defined by a sequence of predicates applied as sort keys, from highest to lowest priority. | Priority | Staking Module | Node Operator | Validator | | -------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------- | ---------------------- | | 1 | | Highest number of validators targeted for **boosted exits** | | | 2 | | Highest number of validators targeted for **smooth exits** | | | 3 | Highest deviation from the exit share limit or the biggest by balance | | | | 4 | | Highest deviation from the **target stake** | | | 5 | | | Lowest validator index | ## Get information to prepare ordered queue In order to prepare a queue of validators to exit, the following actions and considerations involved: - the maximum number of validators that can be requested to exit in one report; - operator network penetration percent - only if the operator's share is greater than 1%; - 'exitable' Lido validators; - fetch node operators stats; - total predictable validators count; - last requested validators indices; ### Report limits - `maxBalanceExitRequestedPerReportInEth` - max total effective balance (in ETH) of validators that can be requested to exit in a single report, from `OracleReportSanityChecker.getOracleReportLimits()`. - `maxValidatorsPerReport` - max number of exit requests allowed in a single report, from the `ValidatorsExitBusOracle` contract (`getMaxValidatorsPerReport()`). Retained alongside the balance limit and checked first. - `VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS` - A parameter from `OracleDaemonConfig` contract used to calculate validators going to exit. - `NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP` - A parameter from `OracleDaemonConfig` that is taken into account when determining the penetration of the operator into the network. ### Get exitable validators A validator is 'exitable' if two conditions are strictly have NOT met: - `validator.exit_epoch != FAR_FUTURE_EPOCH` and - `validator.index <= last_requested_to_exit_index`. ### Node operator stats Statistics for each node operator, which are needed for sorting their validators in exit order: - validators count that are not yet in CL - validators that are in CL and are not yet requested to exit and not on exit - validators that are in CL and requested to exit but not on exit and not requested to exit recently - target type (soft/boosted) and target validators count - checks whether the target limit mode is set NB: A validator can not be considered as delayed if it was requested to exit in last `VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS` slots #### Last requested validators indices The [`ValidatorsExitBusOracle`](/contracts/validators-exit-bus-oracle.md) contract stores the index of the last validator that was requested to exit. Since validators are requested in strict order from the lowest `validatorIndex` to the highest, the indexes help find all the previously requested validators without fetching all events. Returns the latest validator indices that were requested to exit for the given `operator_indexes` in the given `module`. For node operators that were never requested to exit any validator yet, index is set to `-1`. ``` ValidatorsExitBusOracle.getLastRequestedValidatorIndices( uint256 moduleId, uint256[] nodeOpIds ): int256[] ``` ### State collection To find the next validators to exit, Validators Exit Bus Oracle collects the following state from both Ethereum Consensus and Execution layers. - From [OracleDaemonConfig](/contracts/oracle-daemon-config/) contract: - PREDICTION_DURATION_IN_SLOTS - VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS - From [Withdrawal Queue](/contracts/withdrawal-queue-erc721/): - Get total unfinalized withdrawal request amount - From [Lido](/contracts/lido/) contract: - Recent postCLBalance/preCLBalance and withdrawals from Execution Layer Rewards and Withdrawal vaults via events - From Consensus Layer node: - All validators and their states on the reference slot - From [Staking Router](/contracts/staking-router/): - Public keys of all Lido validators - Indices of the last requested validator to exit for each Node Operator - Validator keys statistics for each Node Operator - From Oracle contract: - Maximum number of exit requests for the current frame - Recently requested via Exit Bus public keys to exit ### Fetching data #### Get uncovered withdrawal requests amount of stETH Collects the amount of stETH in the queue yet to be finalized from `WithdrawalQueue.unfinalizedStETH()` #### Calculate average rewards speed per epoch Fetches `ETHDistributed` and `TokenRebased` events from the [`Lido`](/contracts/lido/) contract and calculate average rewards amount per epoch. The rewards prediction period config fetches from the [OracleDaemonConfig](/contracts/oracle-daemon-config/) contract. To get events in past, addressing the cases where there can be slots with missed block, the next scheme is introduced: ![VEBO 6](../../../static/img/oracle-spec/vebo-6.png) - Get from [OracleDaemonConfig](/contracts/oracle-daemon-config/) contract `PREDICTION_DURATION_IN_SLOTS` value - Get `TokenRebased` events from Lido - Get `ETHDistributed` events from Lido - Group that events by transaction hash - Collect from events: - `total_rewards` as `postCLBalance + withdrawalsWithdrawn - preCLBalance executionLayerRewardsWithdrawn` - `time_spent` as sum of each event `timeElapsed` - calculate `rewards_speed_per_epoch` as `max(total_rewards * chain_configs.seconds_per_slot * chain_configs.slots_per_epoch // time_spent, 0)` #### Calculate epochs to sweep ##### Average sweep prediction Predicts the average epochs of the sweep cycle. In the spec: [get expected withdrawals](https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#modified-get_expected_withdrawals), [process withdrawals](https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#modified-process_withdrawals) [source](https://github.com/lidofinance/lido-oracle/blob/master/src/modules/ejector/sweep.py#L40) ##### Withdrawable validators - Check if `validator` has the eth1 withdrawal credentials prefixed with 0x01 *OR* 'compound' withdrawal credentials' prefixed with 0x02, *and* - Check if `validator` is partially withdrawable, *or* - Check if `validator` is fully withdrawable [source](https://github.com/lidofinance/lido-oracle/blob/master/src/modules/ejector/ejector.py#L342) #### Predict available ether before next withdrawn In order to estimate the amount is needed to fully cover the non-finalized withdraw requests, the following values are calculated - **Future rewards** - **Future withdrawals amount** - **Total available balance** - **Validators to eject cumulative amount** - **Going to withdrawn balance** To calculate **future rewards**, it's needed to [predict](https://github.com/lidofinance/lido-oracle/blob/master/src/modules/ejector/ejector.py#L244) an epoch when all validators in queue and `validators_to_eject` will be withdrawn: 1. Calculate latest exit epoch number and amount of validators that are exiting in this epoch 2. If queue is empty - exit epoch will be calculated as `current epoch + MAX_SEED_LOOK AHEAD + 1`. **MAX_SEED_LOOKAHEAD** constant needs to mitigate some attacks, more details [here](https://eth2book.info/bellatrix/part3/config/preset/#max_seed_lookahead) 3. Calculate **churn limit** - like a rate-limit on a balance change in the validator set. Minimum rate is 128 ETH per epoch. The churn limit changes in increments of `EFFECTIVE_BALANCE_INCREMENT = 1 eth`. [spec](https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-get_balance_churn_limit) 4. Calculate slots capacity for exit: ```! remain_exits_capacity_for_epoch=churn_limit - (amount of validators that are exiting in this epoch) ``` 5. Calculate epoch to exit all `validators_to_eject_count`: ```! epochs_required_to_exit_validators = (validators_to_eject_count - remain_exits_capacity_for_epoch) // churn_limit + 1 ``` 6. So the predictable withdrawable epoch: ```! withdrawal_epoch=max_exit_epoch_number + epochs_required_to_exit_validators + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) ``` MIN_VALIDATOR_WITHDRAWABILITY_DELAY [here](https://eth2book.info/altair/part3/config/configuration/#min_validator_withdrawability_delay) So now we can calculate what amount (and validators count) is needed to fully cover amount of non-finalized WithdrawQueue requests. #### Calculate expected balance to withdraw ##### Future rewards ```! future_rewards = (withdrawal_epoch + epochs_to_sweep - blockstamp.ref_epoch ) * rewards_speed_per_epoch ``` ##### Future withdrawals amount Get total balance from validators which can be fully withdrawn. ##### Total available balance Fetch total balance as sum from: - `Lido.getWithdrawalsReserve()` + - Balance from `elRewardsVault` + - Balance from `withdrawalVault` :::note Deposit reserve Since the Staking Router v3 upgrade, a configurable portion of the buffer โ€” the **deposits reserve** โ€” is protected for Consensus Layer deposits and cannot be used to cover withdrawals. The oracle therefore uses `Lido.getWithdrawalsReserve()` (the buffer left after the deposits reserve is set aside) instead of the full `Lido.getBufferedEther()` when estimating the ether already available. Otherwise it would over-count the available buffer and under-request the validators that must be exited. ::: ##### Validators to eject cumulative amount Get balance from next validator in exit queue. ##### Validators going to exit Fetches recently emitted `ValidatorExitRequest` events from `ValidatorsExitBusOracle` contract and extract pubkeys from them. The delayed timeout config fetches from the `OracleDaemonConfig` contract. Validators requested to exit, but didn't send exit message. In case: - Activation epoch is not old enough to initiate exit - Node operator had not enough time to send exit message (VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS) To get validators, oracle calculates: - `lido_validators_by_operator` - Fetches all used Lido keys from [Keys API](https://github.com/lidofinance/lido-keys-api) + Fetches all validators at the reference slot and merge them with keys - `ejected_indexes` - get operators with last exited validator indexes from for all staking_modules and node operators via `ValidatorsExitBusOracle.getLastRequestedValidatorIndices(module_id, uint256[] nodeOpIds)` - `recent_pubkeys` - get last requested to exit pubkeys from `ValidatorExitRequest` event For each `lido_validators_by_operator` oracle tries to find **non exited validators**, so: - if not `validator_asked_to_exit` -> return False - if `is_on_exit` -> return false - if `validator_recently_asked_to_exit` -> return **True** - if not `validator_eligible_to_exit` -> return **True** - otherwise return False Oracle calculates `going_to_withdraw_balance` for all **non exited validators** ##### Compare expected_balance vs to_withdrawn_balance Expected balance is: ``` expected_balance = ( future_withdrawals + # Validators that have withdrawal_epoch future_rewards + # Rewards we get until last validator in validators_to_eject will be withdrawn total_available_balance + # Current EL balance (el vault, wc vault, withdrawals reserve of buffered eth) validator_to_eject_balance_sum + # Validators that we expected to be ejected (requested to exit, not delayed) going_to_withdraw_balance # validators_to_eject balance ) ``` First of all, it's checked without exiting the validator, whether the protocol already has enough available ether to cover withdrawal requests in the queue. If yes, then it's not reasonable to exit validators. If there is not enough, one more validator is considered to be exited and the expected balance gets calculated again. The process continues until the expected balance becomes greater than or equal to the unfinalized withdrawal requests amount. #### Boosted Exits This stage presupposes the exit of validators requiring exit regardless of the demand in WQ. The state of operators and validators after the first step is filtered, leaving only validators of operators having `targetLimitMode` set to boosted exits. Let's consider the target limit modes in more detail: - `0` - **Disabled.** This mode implies no limitation. The operator is not restricted in receiving new stakes and does not have additional priorities when choosing validators for exit. - `1` - **Smooth exit mode.** The operator has a limit on the number of active validators. As long as the number of active validators of the operator does not exceed the `targetLimit`, the operator receives stakes under general conditions. If this value is reached, the operator stops receiving new stakes (should be implemented at the module level). If the number of active keys of the operator exceeds the `targetLimit`, then such an operator's validators are prioritized for exit in the amount of targeted validators to exit. - `2` - **Boosted exit mode.** Similar to smooth mode, but does not consider demand in WQ. The operator's validators in the amount of targeted validators to exit are prioritized for exit and requested without considering demand in WQ. ## Helpful links - [Lido Oracle source code](https://github.com/lidofinance/lido-oracle) --- # Protocol levers Lido V3 governance controls a set of configurable parameters across core pool, oracle, withdrawals, and stVaults. This page summarizes the primary on-chain levers, who can operate them, and provides concrete addresses for role holders. ## Governance structure | Entity | Address | Description | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Aragon Voting | [`0x2e59A20f205bB85a89C53f1936454680651E618e`](https://etherscan.io/address/0x2e59A20f205bB85a89C53f1936454680651E618e) | LDO token voting for protocol governance | | Aragon Agent | [`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c) | Aragon DAO execution agent | | Aragon ACL | [`0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb`](https://etherscan.io/address/0x9895f0f17cc1d1891b6f18ee0b483b6f221b37bb) | Aragon permission registry for AragonApp roles | | Easy Track | [`0xF0211b7660680B49De1A7E9f25C65660F0a13Fea`](https://etherscan.io/address/0xF0211b7660680B49De1A7E9f25C65660F0a13Fea) | Optimistic governance for routine operations | | Easy Track EVMScript Executor | [`0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977`](https://etherscan.io/address/0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977) | Executes Easy Track motions | | Vaults Adapter | [`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27) | stVaults adapter that holds VaultHub/OperatorGrid roles | | CircuitBreaker Committee | [`0x8772E3a2D86B9347A2688f9bc1808A6d8917760C`](https://etherscan.io/address/0x8772E3a2D86B9347A2688f9bc1808A6d8917760C) | Emergency pause signer via the CircuitBreaker | | Reseal Manager | [`0x7914b5a1539b97Bd0bbd155757F25FD79A522d24`](https://etherscan.io/address/0x7914b5a1539b97Bd0bbd155757F25FD79A522d24) | Pause extension authority for CircuitBreaker-paused contracts under DualGovernance veto escalated states | ## Upgradeability All protocol proxy admins are set to the **Lido DAO Agent** ([`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c)). Upgrades require a successful DAO vote. Upgradeable core protocol proxies (mainnet): | Contract | Address | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | [LidoLocator](/contracts/lido-locator/) | [`0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb`](https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb) | | [Lido](/contracts/lido/) | [`0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84`](https://etherscan.io/address/0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84) | | [Accounting](/contracts/accounting/) | [`0x23ED611be0e1a820978875C0122F92260804cdDf`](https://etherscan.io/address/0x23ED611be0e1a820978875C0122F92260804cdDf) | | [StakingRouter](/contracts/staking-router/) | [`0xFdDf38947aFB03C621C71b06C9C70bce73f12999`](https://etherscan.io/address/0xFdDf38947aFB03C621C71b06C9C70bce73f12999) | | [WithdrawalQueueERC721](/contracts/withdrawal-queue-erc721/) | [`0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1`](https://etherscan.io/address/0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1) | | [WithdrawalVault](/contracts/withdrawal-vault/) | [`0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f`](https://etherscan.io/address/0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f) | | [Burner](/contracts/burner/) | [`0xE76c52750019b80B43E36DF30bf4060EB73F573a`](https://etherscan.io/address/0xE76c52750019b80B43E36DF30bf4060EB73F573a) | | [VaultHub](/contracts/vault-hub/) | [`0x1d201BE093d847f6446530Efb0E8Fb426d176709`](https://etherscan.io/address/0x1d201BE093d847f6446530Efb0E8Fb426d176709) | | [PredepositGuarantee](/contracts/predeposit-guarantee/) | [`0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3`](https://etherscan.io/address/0xF4bF42c6D6A0E38825785048124DBAD6c9eaaac3) | | [OperatorGrid](/contracts/operator-grid/) | [`0xC69685E89Cefc327b43B7234AC646451B27c544d`](https://etherscan.io/address/0xC69685E89Cefc327b43B7234AC646451B27c544d) | Upgradeable oracle proxies (mainnet): | Contract | Address | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | [AccountingOracle](/contracts/accounting-oracle/) | [`0x852deD011285fe67063a08005c71a85690503Cee`](https://etherscan.io/address/0x852deD011285fe67063a08005c71a85690503Cee) | | [ValidatorsExitBusOracle](/contracts/validators-exit-bus-oracle/) | [`0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e`](https://etherscan.io/address/0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e) | | [LazyOracle](/contracts/lazy-oracle/) | [`0x5DB427080200c235F2Ae8Cd17A7be87921f7AD6c`](https://etherscan.io/address/0x5DB427080200c235F2Ae8Cd17A7be87921f7AD6c) | Upgradeable staking module proxies (mainnet): | Contract | Address | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Curated Node Operators Registry | [`0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5`](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5) | | Simple DVT Node Operators Registry | [`0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433`](https://etherscan.io/address/0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433) | | Community Staking Module | [`0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F`](https://etherscan.io/address/0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F) | | Community Staking Accounting | [`0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da`](https://etherscan.io/address/0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da) | | Community Staking Parameters Registry | [`0x9D28ad303C90DF524BA960d7a2DAC56DcC31e428`](https://etherscan.io/address/0x9D28ad303C90DF524BA960d7a2DAC56DcC31e428) | | Community Staking Fee Distributor | [`0xD99CC66fEC647E68294C6477B40fC7E0F6F618D0`](https://etherscan.io/address/0xD99CC66fEC647E68294C6477B40fC7E0F6F618D0) | | Community Staking Fee Oracle | [`0x4D4074628678Bd302921c20573EEa1ed38DdF7FB`](https://etherscan.io/address/0x4D4074628678Bd302921c20573EEa1ed38DdF7FB) | | Community Staking Strikes | [`0xaa328816027F2D32B9F56d190BC9Fa4A5C07637f`](https://etherscan.io/address/0xaa328816027F2D32B9F56d190BC9Fa4A5C07637f) | | Community Staking Exit Penalties | [`0x06cd61045f958A209a0f8D746e103eCc625f4193`](https://etherscan.io/address/0x06cd61045f958A209a0f8D746e103eCc625f4193) | ## Lido (core pool) Key levers on the core pool contract [Lido](/contracts/lido/) ([`0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84`](https://etherscan.io/address/0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84)): | Lever | Mutators | Role | Role registry | Role admin | Holder | | ------------------- | ------------------------------------------- | ---------------------- | ------------- | ------------ | ---------- | | Pause protocol | `stop()` | `PAUSE_ROLE` | Aragon ACL | Aragon Agent | Unassigned | | Resume protocol | `resume()` | `RESUME_ROLE` | Aragon ACL | Aragon Agent | Unassigned | | Staking limits | `setStakingLimit()`, `removeStakingLimit()` | `STAKING_CONTROL_ROLE` | Aragon ACL | Aragon Agent | Unassigned | | External shares cap | `setMaxExternalRatioBP()` | `STAKING_CONTROL_ROLE` | Aragon ACL | Aragon Agent | Unassigned | Roles marked as unassigned are intentionally left without holders. The DAO can assign them later through Aragon ACL governance; see the [permissions transition guide](https://github.com/lidofinance/dual-governance/blob/main/docs/permissions-transition/permissions-transition-mainnet.md) for design context (prepared pre-V3 but still applicable on principles). ### Emergency pause The [CircuitBreaker](/contracts/circuit-breaker) allows emergency pausing without a full DAO vote. It can temporarily pause registered contracts, while the Reseal Manager holds both the `PAUSE_ROLE` and `RESUME_ROLE` for pause extension. For the current set of covered contracts and their pausers, see the [CircuitBreaker covered pausables](/deployed-contracts/#circuit-breaker). CircuitBreaker pauses are time-limited; the Reseal Manager can extend a pause window, and resuming requires a DAO vote that unpauses the app. ## Accounting and oracles | Lever | Contract | Mutators | Role | Role registry | Role admin | Holder | | -------------------------- | -------------------------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------- | ------------------------- | ------------ | ---------- | | Consensus settings | [AccountingOracle](/contracts/accounting-oracle/) | `setConsensusVersion()`, `setConsensusContract()` | `MANAGE_CONSENSUS_VERSION_ROLE`, `MANAGE_CONSENSUS_CONTRACT_ROLE` | AccountingOracle | Aragon Agent | Unassigned | | Oracle report bounds | [OracleReportSanityChecker](/contracts/oracle-report-sanity-checker/) | Various limit setters | `ALL_LIMITS_MANAGER_ROLE` | OracleReportSanityChecker | Aragon Agent | Unassigned | | LazyOracle sanity | [LazyOracle](/contracts/lazy-oracle/) | `updateSanityParams()` | `UPDATE_SANITY_PARAMS_ROLE` | LazyOracle | Aragon Agent | Unassigned | | Oracle daemon config | [OracleDaemonConfig](/contracts/oracle-daemon-config/) | `setConfig()` | `CONFIG_MANAGER_ROLE` | OracleDaemonConfig | Aragon Agent | Unassigned | | VEB report data submission | [ValidatorsExitBusOracle](/contracts/validators-exit-bus-oracle/) | `submitReportData()` | `SUBMIT_DATA_ROLE` | ValidatorsExitBusOracle | Aragon Agent | Unassigned | ### Oracle consensus Oracle reports are submitted through HashConsensus contracts: | Oracle | HashConsensus Address | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | [AccountingOracle](/contracts/accounting-oracle/) | [`0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288`](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288) | | [ValidatorsExitBusOracle](/contracts/validators-exit-bus-oracle/) | [`0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a`](https://etherscan.io/address/0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a) | ## StakingRouter and modules Key levers on [StakingRouter](/contracts/staking-router/) ([`0xFdDf38947aFB03C621C71b06C9C70bce73f12999`](https://etherscan.io/address/0xFdDf38947aFB03C621C71b06C9C70bce73f12999)): | Lever | Mutators | Role | Role registry | Role admin | Holder | | ---------------------- | ------------------------------------------------------------------------- | ------------------------------------ | ------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Module registry | `addStakingModule()`, `updateStakingModule()`, `setStakingModuleStatus()` | `STAKING_MODULE_MANAGE_ROLE` | StakingRouter | Aragon Agent | Aragon Agent ([`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c)) | | Module fees | `setStakingModuleFees()` | `STAKING_MODULE_MANAGE_ROLE` | StakingRouter | Aragon Agent | Aragon Agent ([`0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c`](https://etherscan.io/address/0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c)) | | Withdrawal credentials | `setWithdrawalCredentials()` | `MANAGE_WITHDRAWAL_CREDENTIALS_ROLE` | StakingRouter | Aragon Agent | Unassigned | | Module unvetting | `decreaseStakingModuleVettedKeysCountByNodeOperator()` | `STAKING_MODULE_UNVETTING_ROLE` | StakingRouter | Aragon Agent | [DepositSecurityModule](/contracts/deposit-security-module/) ([`0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD`](https://etherscan.io/address/0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD)) | ### Active staking modules | Module | Registry Address | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Curated (NodeOperatorsRegistry) | [`0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5`](https://etherscan.io/address/0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5) | | SimpleDVT | [`0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433`](https://etherscan.io/address/0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433) | | Community Staking (CSM) | [`0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F`](https://etherscan.io/address/0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F) | ## Withdrawals Key levers on [WithdrawalQueueERC721](/contracts/withdrawal-queue-erc721/) ([`0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1`](https://etherscan.io/address/0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1)): | Lever | Mutators | Role | Role registry | Role admin | Holder | | ----------- | ------------------ | ------------- | --------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Pause | `pauseFor()` | `PAUSE_ROLE` | WithdrawalQueueERC721 | Aragon Agent | CircuitBreaker ([`0x6019CB557978296BA3C08a7B73225C0975DFB2F7`](https://etherscan.io/address/0x6019CB557978296BA3C08a7B73225C0975DFB2F7)), Reseal Manager ([`0x7914b5a1539b97Bd0bbd155757F25FD79A522d24`](https://etherscan.io/address/0x7914b5a1539b97Bd0bbd155757F25FD79A522d24)) | | Resume | `resume()` | `RESUME_ROLE` | WithdrawalQueueERC721 | Aragon Agent | Reseal Manager ([`0x7914b5a1539b97Bd0bbd155757F25FD79A522d24`](https://etherscan.io/address/0x7914b5a1539b97Bd0bbd155757F25FD79A522d24)) | | Bunker mode | `onOracleReport()` | `ORACLE_ROLE` | WithdrawalQueueERC721 | Aragon Agent | AccountingOracle ([`0x852deD011285fe67063a08005c71a85690503Cee`](https://etherscan.io/address/0x852deD011285fe67063a08005c71a85690503Cee)) | ## stVaults (VaultHub + OperatorGrid) ### VaultHub Key levers on [VaultHub](/contracts/vault-hub/) ([`0x1d201BE093d847f6446530Efb0E8Fb426d176709`](https://etherscan.io/address/0x1d201BE093d847f6446530Efb0E8Fb426d176709)): | Lever | Mutators | Role | Role registry | Role admin | Holder | | ----------------- | ------------------------------------------------------ | ------------------------ | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Vault connections | `connectVault()`, `updateConnection()`, `disconnect()` | `VAULT_MASTER_ROLE` | VaultHub | Aragon Agent | Unassigned | | Redemptions | `setLiabilitySharesTarget()` | `REDEMPTION_MASTER_ROLE` | VaultHub | Aragon Agent | Unassigned | | Bad debt handling | `socializeBadDebt()`, `internalizeBadDebt()` | `BAD_DEBT_MASTER_ROLE` | VaultHub | Aragon Agent | Vaults Adapter ([`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27)) | | Forced exits | `forceValidatorExit()` | `VALIDATOR_EXIT_ROLE` | VaultHub | Aragon Agent | Vaults Adapter ([`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27)) | | Pause | `pauseFor()` | `PAUSE_ROLE` | VaultHub | Aragon Agent | CircuitBreaker ([`0x6019CB557978296BA3C08a7B73225C0975DFB2F7`](https://etherscan.io/address/0x6019CB557978296BA3C08a7B73225C0975DFB2F7)), Reseal Manager ([`0x7914b5a1539b97Bd0bbd155757F25FD79A522d24`](https://etherscan.io/address/0x7914b5a1539b97Bd0bbd155757F25FD79A522d24)) | | Resume | `resume()` | `RESUME_ROLE` | VaultHub | Aragon Agent | Reseal Manager ([`0x7914b5a1539b97Bd0bbd155757F25FD79A522d24`](https://etherscan.io/address/0x7914b5a1539b97Bd0bbd155757F25FD79A522d24)) | ### OperatorGrid Key levers on [OperatorGrid](/contracts/operator-grid/) ([`0xC69685E89Cefc327b43B7234AC646451B27c544d`](https://etherscan.io/address/0xC69685E89Cefc327b43B7234AC646451B27c544d)): | Lever | Mutators | Role | Role registry | Role admin | Holder | | ------------------ | -------------------------------------------- | --------------- | ------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Group registration | `registerGroup()`, `updateGroupShareLimit()` | `REGISTRY_ROLE` | OperatorGrid | Aragon Agent | Easy Track EVMScript Executor ([`0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977`](https://etherscan.io/address/0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977)), Vaults Adapter ([`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27)) | | Tier management | `registerTiers()`, `alterTiers()` | `REGISTRY_ROLE` | OperatorGrid | Aragon Agent | Easy Track EVMScript Executor ([`0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977`](https://etherscan.io/address/0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977)), Vaults Adapter ([`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27)) | | Vault fees | `updateVaultFees()` | `REGISTRY_ROLE` | OperatorGrid | Aragon Agent | Easy Track EVMScript Executor ([`0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977`](https://etherscan.io/address/0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977)), Vaults Adapter ([`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27)) | | Jail status | `setVaultJailStatus()` | `REGISTRY_ROLE` | OperatorGrid | Aragon Agent | Easy Track EVMScript Executor ([`0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977`](https://etherscan.io/address/0xFE5986E06210aC1eCC1aDCafc0cc7f8D63B3F977)), Vaults Adapter ([`0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27`](https://etherscan.io/address/0x28F9Ac198C4E0FA6A9Ad2c2f97CB38F1A3120f27)) | ## Dual Governance Lido V3 includes a Dual Governance system allowing stETH holders to veto DAO decisions: | Component | Address | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | Dual Governance | [`0xC1db28B3301331277e307FDCfF8DE28242A4486E`](https://etherscan.io/address/0xC1db28B3301331277e307FDCfF8DE28242A4486E) | | Emergency Protected Timelock | [`0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316`](https://etherscan.io/address/0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316) | | Veto Signaling Escrow | [`0x165813A31446a98c84E20Dda8C101BB3C8228e1c`](https://etherscan.io/address/0x165813A31446a98c84E20Dda8C101BB3C8228e1c) | | Emergency Activation Committee | [`0x8B7854488Fde088d686Ea672B6ba1A5242515f45`](https://etherscan.io/address/0x8B7854488Fde088d686Ea672B6ba1A5242515f45) | | Emergency Execution Committee | [`0xC7792b3F2B399bB0EdF53fECDceCeB97FBEB18AF`](https://etherscan.io/address/0xC7792b3F2B399bB0EdF53fECDceCeB97FBEB18AF) | ## References - [Deployed contracts (mainnet)](/deployed-contracts/) - [AccountingOracle](/contracts/accounting-oracle/) - [OracleReportSanityChecker](/contracts/oracle-report-sanity-checker/) - [StakingRouter](/contracts/staking-router/) - [WithdrawalQueueERC721](/contracts/withdrawal-queue-erc721/) - [VaultHub](/contracts/vault-hub/) - [OperatorGrid](/contracts/operator-grid/) - [Emergency Brakes Multisigs](/multisigs/emergency-brakes/) --- # Reward distribution bot ## Introduction Permissionless reward distribution bot for Lido staking modules. Operates with smart contract based on the [Node Operator Registry](/contracts/node-operators-registry/) smart contract. After the [Accounting Oracle](/guides/oracle-spec/accounting-oracle/) completes the third phase, anyone can initiate reward distribution to allocate rewards among Node Operators in the Staking Module, unless the oracle sends the next frame report. ## Requirements ### Hardware - 1-core CPU - 2GB RAM ### Nodes - Execution Node (can be an RPC) ## How to use Every epoch daemon checks the staking modules provided in environment. If a module has a non-distributed rewards, the bot pulls the trigger and distributes rewards between Node Operators inside this module. Basically bot is watching [RewardDistributionState](/contracts/node-operators-registry/#getrewarddistributionstate) of module. If module has `ReadyForDistribution` state, bot triggers [distributeReward](/contracts/node-operators-registry/#distributereward) method to distribute rewards. ### Envs Required variables are: - WEB3_RPC_ENDPOINTS - list of EL rpc endpoints separated by `,`. All except the first rpc will serve as fallback options. - NODE_OPERATOR_REGISTRY_ADDRESSES - List of staking modules that can accept rewards distribution. All addresses could be found [here](/deployed-contracts/). - WALLET_PRIVATE_KEY - Wallet private key that will send such transactions. Do not provide to run bot in DRY mode (do not send transaction). Optional variables can be found [here](https://github.com/lidofinance/nor-reward-distribution-bot?tab=readme-ov-file#optional). ## Running ### Source Code 1. Clone repository and install requirements: ```bash git clone git@github.com:lidofinance/nor-reward-distribution-bot.git cd nor-reward-distribution-bot ``` 2. Install requirements ```bash poetry install ``` 3. Run distributor bot ```bash poetry run python src/main.py ``` ### Docker Docker image could be found [here](/guides/tooling/#reward-distribution-bot). ## Monitoring Prometheus metrics will be available on endpoint `http://localhost:${PROMETHEUS_PORT}/metrics`. Alerts [source code](https://github.com/lidofinance/nor-reward-distribution-bot/blob/main/alerts/alerts.yml) for AlertManager. --- # Solana manual withdrawal with CLI ## 1. Environment Setup We've prepared a CLI in Solido to simplify your workflow. You'll need to: 1. **Install Rust**: Follow the instructions at [Rust Installation](https://www.rust-lang.org/tools/install). ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" rustup override set 1.60.0 ``` 2. **Install Solana CLI v1.13.7**: Visit [Solana CLI Installation](https://solana.com/docs/intro/installation). ```bash sh -c "$(curl -sSfL https://release.solana.com/v1.13.7/install)" ``` 3. **Install Solido CLI v2.1.0** from the official GitHub repository: See [Solido Releases](https://github.com/lidofinance/solido/releases/tag/v2.1.0). ```bash git clone --recurse-submodules -b V2.1 https://github.com/lidofinance/solido solido_v2 cd solido_v2 cargo build --release ``` :::warning Compilation error with anchor-lang If the build fails due to a broken submodule in the `anchor-lang` dependency, you need to create a fork with the fix: ```bash git clone -b solana-1.9.28 https://github.com/AnchorLang/anchor-solana1.9.28.git cd anchor-solana1.9.28 git submodule deinit -f examples/cfo/deps/stake || true git rm -f examples/cfo/deps/stake || true rm -rf .git/modules/examples/cfo/deps/stake || true git commit -m "Remove broken stake submodule" git remote set-url origin https://github.com/YOUR_GITHUB_USERNAME/anchor-solana1.9.28.git git push origin solana-1.9.28 ``` Then update `Cargo.toml` in the `solido_v2` directory, replacing the `anchor-lang` dependency with your fork: ```toml anchor-lang = { git = "https://github.com/YOUR_GITHUB_USERNAME/anchor-solana1.9.28", branch = "solana-1.9.28" } ``` After that, re-run `cargo build --release`. ::: ## 2. Transfer stSOL to Local Account โš ๏ธ **Note**: Our CLI can only work with local keys. Consider using a new account for withdrawals to keep your main wallet secure. The withdrawal operation will utilize the following: - **`SOL_ACCOUNT_PUBKEY`**: Public key of the local Solana account. - **`STSOL_ACCOUNT_PUBKEY`**: Public key of the child stSOL account from **`SOL_ACCOUNT_PUBKEY`**. - **`KEYPAIR_FILE`**: Local file containing the keypair from **`SOL_ACCOUNT_PUBKEY`**. - **`STAKE_ACCOUNT_PUBKEY`**: Public key of the child stake account from **`SOL_ACCOUNT_PUBKEY`**. 1. **Create a new local account**: ```bash solana-keygen new --outfile ./local-keypair.json ``` Remember the **`KEYPAIR_FILE`** path and **`SOL_ACCOUNT_PUBKEY`** from the output. 2. **Configure RPC endpoint**: The default Solana RPC endpoint may not work reliably for mainnet operations. Set a dedicated RPC URL (e.g., from [Helius](https://www.helius.dev/)): ```bash solana config set --url https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY ``` 3. **Verify the new account with `SOL_ACCOUNT_PUBKEY`**: ```bash solana balance SOL_ACCOUNT_PUBKEY ``` 4. **Transfer stSOL to the local account using `SOL_ACCOUNT_PUBKEY`** and **note the transaction signature**. 5. **Identify `STSOL_ACCOUNT_PUBKEY`**: โš ๏ธ After transferring stSOL, a child account for stSOL is created under your local account. To proceed, locate this address by searching your **transaction signature** on [Solscan](https://solscan.io/) and saving the **Destination** pubkey found under **Token Program: TransferChecked** in the instruction details. ![STSOL_ACCOUNT_PUBKEY](./images/stsol_account_pubkey.png) ## 3. Withdraw stSOL 1. **Execute the withdrawal** to your stake account: ```bash ./target/release/solido --config ./solido_config.json --keypair-path KEYPAIR_FILE withdraw --amount-st-sol STSOL_AMOUNT ``` โš ๏ธ **Note**: If you're seeing the following error: ```bash Program log: The exchange rate is outdated, it was last computed in epoch 644, but now it is epoch 646. ``` Execute the following command to update the exchange rate: ```bash ./target/release/solido --config ./solido_config.json --keypair-path KEYPAIR_FILE perform-maintenance ``` If you get an RPC response error, such as -32002, simply re-run the command. Once the exchange rate is updated, you should get a message "Updated exchange rate", and you can proceed to re-run the withdrawal command. 2. Record the **`STAKE_ACCOUNT_PUBKEY`** for further steps. 3. **Deactivate the stake account**: ```bash solana deactivate-stake STAKE_ACCOUNT_PUBKEY --keypair KEYPAIR_FILE ``` โš ๏ธ Wait for the epoch to end (~1-2 days) for the stake account to become inactive. Check the epoch status on [Solana Explorer](https://explorer.solana.com/). ## 4. Transfer SOL to Main Account After the epoch ends, withdraw SOL from **`STAKE_ACCOUNT_PUBKEY`** to your main account, referred to as **`MAIN_ACCOUNT_PUBKEY`**. ```bash solana withdraw-stake **STAKE_ACCOUNT_PUBKEY** \ **MAIN_ACCOUNT_PUBKEY** SOL_AMOUNT \ --keypair **KEYPAIR_FILE** ``` --- # Off-chain Components Overview Overview of core infrastructure components used in the Lido protocol. ## Oracle Oracle daemon for Lido decentralized staking service. - **Version**: 8.1.0 - **Docker image**: sha256:ea496996214d309a6fcf9e8231627b1486cf1f92439571a1773b16fc3f2af9f4, [lidofinance/oracle@sha256-ea496996214d309a6fcf9e8231627b1486cf1f92439571a1773b16fc3f2af9f4](https://hub.docker.com/layers/lidofinance/oracle/8.1.0/images/sha256-ea496996214d309a6fcf9e8231627b1486cf1f92439571a1773b16fc3f2af9f4) - **Commit hash**: [lidofinance/lido-oracle@032c228](https://github.com/lidofinance/lido-oracle/commit/032c228c767759e67da43e6c40fa81732257879d) - **Last update date**: 8 September, 2026 - [**Repository**](https://github.com/lidofinance/lido-oracle/tree/8.1.0) - [**Documentation**](/guides/oracle-operator-manual) - [**Audit Report for v8.0.1 (Composable Security)**](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8%20Audit%20Report.pdf) - [**Audit Report for v8.0.2 (Composable Security)**](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_0_2%20Security%20Consultation%20Report.pdf) - [**Audit Report for v8.0.3 (Composable Security)**](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_0_3%20Security%20Consultation%20Report.pdf) - [**Audit Report for v8.0.5 (Composable Security)**](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_0_5%20Security%20Consultation%20Report.pdf) - [**Audit Report for v8.0.5 (MixBytes)**](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lido%20Oracle%20v8.0.5%20Security%20Audit%20Report%2007-2026.pdf) - [**Audit Report for v8.0.6 (MixBytes)**](https://github.com/lidofinance/audits/blob/main/MixBytes%20Lido%20Oracle%20v8.0.6%20Security%20Audit%20Report%2008-2026.pdf) - [**Audit Report for v8.1 (Composable Security)**](https://github.com/lidofinance/audits/blob/main/Composable%20Security%20Lido%20Oracle%20V8_1%20Audit%20Report.pdf) ## Validator Ejector Daemon service which loads LidoOracle events for validator exits and sends out exit messages when necessary. - **Version**: 2.1.0 - **Docker image**: sha256:8953a4107d99ab84ff0f2b02cb7dd13b7cd7e5a565cf04fbe36e7911df5983dc, [lidofinance/validator-ejector@sha256-8953a4107d99ab84ff0f2b02cb7dd13b7cd7e5a565cf04fbe36e7911df5983dc](https://hub.docker.com/layers/lidofinance/validator-ejector/2.1.0/images/sha256-8953a4107d99ab84ff0f2b02cb7dd13b7cd7e5a565cf04fbe36e7911df5983dc) - **Commit hash**: [lidofinance/validator-ejector@ec0992d](https://github.com/lidofinance/validator-ejector/commit/ec0992d9b4454425470b6608336755419ddb94ca) - **Last update date**: 26 May, 2026 - [**Repository**](https://github.com/lidofinance/validator-ejector/tree/2.1.0) - [**Documentation**](/guides/validator-ejector-guide) ## Council daemon The Lido Council Daemon monitors deposit contract keys. - **Version**: 4.0.4 - **Docker image**: sha256:8e419905599b55cf37dc51f667468e7a24c34e7b5bade17e7f08691e98dbdb02, [lidofinance/lido-council-daemon@sha256-8e419905599b55cf37dc51f667468e7a24c34e7b5bade17e7f08691e98dbdb02](https://hub.docker.com/layers/lidofinance/lido-council-daemon/4.0.4/images/sha256-8e419905599b55cf37dc51f667468e7a24c34e7b5bade17e7f08691e98dbdb02) - **Commit hash**: [lidofinance/lido-council-daemon@b02577f](https://github.com/lidofinance/lido-council-daemon/commit/b02577ff193ea8fa96f5c16025292d044ebd70f3) - **Last update date**: 7 July, 2026 - [**Repository**](https://github.com/lidofinance/lido-council-daemon/tree/4.0.4) - [**Documentation**](/guides/deposit-security-manual) ## Depositor Bot Bot that submits deposit transactions to the Lido protocol once the Deposit Security Committee quorum is reached. - **Version**: 5.6.0 - **Docker image**: sha256:a8fc015713cf4680bf2d2692de7a295ac99d00d29bb154860c285a44e63e0c32, [lidofinance/depositor-bot@sha256-a8fc015713cf4680bf2d2692de7a295ac99d00d29bb154860c285a44e63e0c32](https://hub.docker.com/layers/lidofinance/depositor-bot/5.6.0/images/sha256-a8fc015713cf4680bf2d2692de7a295ac99d00d29bb154860c285a44e63e0c32) - **Commit hash**: [lidofinance/depositor-bot@ccb788e](https://github.com/lidofinance/depositor-bot/commit/ccb788e041cf7a95ff5f9a1894bb67fd5393124c) - **Last update date**: 24 July, 2026 - [**Repository**](https://github.com/lidofinance/depositor-bot/tree/5.6.0) - [**Documentation**](/guides/depositor-bot) ## Reward Distribution Bot Bot that distributes node-operator rewards in the Curated and Simple DVT staking modules. - **Version**: 1.1.0 - **Docker image**: sha256:610609ad79a31bd4973299f3744170199732ad8456a18dccd19a9a5b5798977e, [lidofinance/nor-reward-distribution-bot@sha256-610609ad79a31bd4973299f3744170199732ad8456a18dccd19a9a5b5798977e](https://hub.docker.com/layers/lidofinance/nor-reward-distribution-bot/1.1.0/images/sha256-610609ad79a31bd4973299f3744170199732ad8456a18dccd19a9a5b5798977e) - **Commit hash**: [lidofinance/nor-reward-distribution-bot@1e37b0a](https://github.com/lidofinance/nor-reward-distribution-bot/commit/1e37b0abb72200cbfed6590704e0bdab3da789dc) - **Last update date**: 21 April, 2026 - [**Repository**](https://github.com/lidofinance/nor-reward-distribution-bot/tree/1.1.0) - [**Documentation**](/guides/reward-distributor-bot) ## Keys API Lido keys HTTP API. - **Version**: 4.0.1 - **Docker image**: sha256:7ea121b25b1b68f805cf2a7dd5094052be4a53c8dabdc7f3aa728c0318a90a1e, [lidofinance/lido-keys-api@sha256-7ea121b25b1b68f805cf2a7dd5094052be4a53c8dabdc7f3aa728c0318a90a1e](https://hub.docker.com/layers/lidofinance/lido-keys-api/4.0.1/images/sha256-7ea121b25b1b68f805cf2a7dd5094052be4a53c8dabdc7f3aa728c0318a90a1e) - **Commit hash**: [lidofinance/lido-keys-api@f347ed5](https://github.com/lidofinance/lido-keys-api/commit/f347ed570c74a90456c6302a8f3e2168ae900675) - **Last update date**: 8 July, 2026 - [**Repository**](https://github.com/lidofinance/lido-keys-api/tree/4.0.1) - [**Documentation**](/guides/kapi-guide) ## Validator Exit Bot Bot that automates triggering exits for Lido validators that have missed their exit deadline using [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) triggerable exits. - **Version**: 1.0.1 - **Docker image**: sha256:0a649a5eff41a9c05ee82bc974ba4b40943ea2225c37568d52a9f3416f75f31c, [lidofinance/validator-exit-bot@sha256-0a649a5eff41a9c05ee82bc974ba4b40943ea2225c37568d52a9f3416f75f31c](https://hub.docker.com/layers/lidofinance/validator-exit-bot/1.0.1/images/sha256-0a649a5eff41a9c05ee82bc974ba4b40943ea2225c37568d52a9f3416f75f31c) - **Commit hash**: [lidofinance/validator-exit-bot@edf5daf](https://github.com/lidofinance/validator-exit-bot/commit/edf5daf684f48f8a2b989e49dde0f2afc72565f8) - **Last update date**: 9 February, 2026 - [**Repository**](https://github.com/lidofinance/validator-exit-bot/tree/1.0.1) - [**Documentation**](/guides/validator-exit-bot) ## Late Prover Bot Bot that monitors the beacon chain for validators that missed their exit deadline and submits Merkle proofs of the delay to the `ValidatorExitDelayVerifier` contract. - **Version**: 1.0.5 - **Docker image**: sha256:eb23b4fb757dcdc9d2c418941a2a29ce3513a3800d20e3fa162a594519b7f52c, [lidofinance/late-prover-bot@sha256-eb23b4fb757dcdc9d2c418941a2a29ce3513a3800d20e3fa162a594519b7f52c](https://hub.docker.com/layers/lidofinance/late-prover-bot/1.0.5/images/sha256-eb23b4fb757dcdc9d2c418941a2a29ce3513a3800d20e3fa162a594519b7f52c) - **Commit hash**: [lidofinance/late-prover-bot@59d102a](https://github.com/lidofinance/late-prover-bot/commit/59d102a25c096e21c76798d0bdae7cae0ba65f56) - **Last update date**: 28 April, 2026 - [**Repository**](https://github.com/lidofinance/late-prover-bot/tree/1.0.5) - [**Documentation**](/guides/late-prover-bot) --- # Validator Ejector ## Introduction Ejector is a daemon service which monitors [ValidatorsExitBusOracle](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol) events and sends out stored exit messages when necessary. It allows Node Operators to generate and sign exit messages ahead of time, which will be sent out by the Ejector when the Protocol requests an exit to be made. On start, it loads exit messages from a specified folder in form of individual `.json` files and validates their format, structure and signature. Then, it loads events from a configurable amount of latest finalized blocks, checks if exits should be made and after that periodically fetches fresh events. ## Requirements ### Hardware - 2-core CPU - 1GB RAM ### Nodes - Execution Node - [Full node required](https://ethereum.org/en/developers/docs/nodes-and-clients/#node-types) - Consensus Node ### Software #### Using Docker: Docker + docker-compose. #### Running directly or using for message encryption: Node.js 16. ## Exit Messages Ejector loads and validates exit messages on start. This means that any changes to the messages folder (eg new exit messages) require a restart of the app to be picked up. Ejector accepts messages in three formats: ### Generic Format ```json { "message": { "epoch": "123", "validator_index": "123" }, "signature": "0x123" } ``` ### ethdo Output Format ```json { "exit": { "message": { "epoch": "123", "validator_index": "123" }, "signature": "0x123" }, "fork_version": "0x123" } ``` ### Encrypted Format ```json { "version": 4, "uuid": "123abc-123abc-123abc", "path": "", "pubkey": "", "crypto": { "kdf": { "function": "pbkdf2", "params": { "dklen": 123, "c": 123, "prf": "hmac-sha256", "salt": "123abc" }, "message": "" }, "checksum": { "function": "sha256", "params": {}, "message": "123abc" }, "cipher": { "function": "aes-128-ctr", "params": { "iv": "123abc" }, "message": "123abc" } } } ``` ## Encrypting Messages It is highly advised that after exit messages are generated and signed, they should be encrypted for storage safety. Ejector will decrypt files on start by looking up the password in `MESSAGES_PASSWORD` environment variable. Exit messages are encrypted and decrypted by Ejector following the [EIP-2335](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2335.md) spec. Ejector is bundled with a small, easy to use encryption script. ### Encryption using Ejector - Source Code 1. Clone repository: ```bash git clone https://github.com/lidofinance/validator-ejector.git cd validator-ejector ``` 2. Create `.env` file with encryption password or pass before the command: ``` MESSAGES_PASSWORD=password ``` 3. Copy JSON exit message files to `encryptor/input` 4. Run `yarn & yarn encrypt` 5. Encrypted exit message files will be saved to `encryptor/output` ### Encryption using Ejector - Docker Ejector is bundled with encryptor script inside, so you can run it using the same Docker image: ```bash docker run \ -e MESSAGES_PASSWORD=secret \ -v /full/path/to/input:/app/encryptor/input/ \ -v /full/path/to/output:/app/encryptor/output/ \ lidofinance/validator-ejector@sha256: \ node /app/dist/encryptor/encrypt.js ``` You can find a recommended version's hash [here](/guides/tooling/). For platforms with a different architecture but with emulation/transpilation support eg macOS on M processors, additionally specify: ```bash --platform linux/amd64 ``` ## Env Variables ### EXECUTION_NODE Address of the Execution Node. ### CONSENSUS_NODE Address of the Consensus Node. ### LOCATOR_ADDRESS Address of the [LidoLocator](https://github.com/lidofinance/lido-dao/blob/feature/shapella-upgrade/contracts/0.8.9/LidoLocator.sol) contract: [Hoodi](/deployed-contracts/hoodi/) / [Mainnet](/deployed-contracts/) ### STAKING_MODULE_ID ID of the [StakingRouter](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/StakingRouter.sol) contract module. Currently, it has only one module ([NodeOperatorsRegistry](https://github.com/lidofinance/lido-dao/blob/feature/shapella-upgrade/contracts/0.4.24/nos/NodeOperatorsRegistry.sol)), it's id is `1`. ### OPERATOR_ID You can find it on the Operators Dashboard (`#123` on the operator card): [Holeลกky](https://operators-holesky.testnet.fi) / [Mainnet](https://operators.lido.fi) ### MESSAGES_LOCATION Location from which to load `.json` exit messages from. When set, messages mode will be activated. Not needed if you are using the Ejector in webhook mode. For example, `/messages` in Docker or simply `messages` if running directly for local files. External storage bucket url is also supported for AWS S3 and Google Cloud Storage: - `s3://` for S3 - `gs://` for GCS Authentication setup: [GCS](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa), [S3](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html). ### VALIDATOR_EXIT_WEBHOOK Endpoint to fetch when an exit has to be made. Allows to implement JIT approach by offloading exiting logic to an external service and using the Ejector as a secure exit events reader. When set, webhook mode will be activated. Not needed if you are using the Ejector in messages mode. On the endpoint, JSON will be POSTed with the following structure: ```json { "validatorIndex": "123", "validatorPubkey": "0x123" } ``` 200 response will be counted as a successful exit, non-200 as a fail. ### ORACLE_ADDRESSES_ALLOWLIST JSON array of Lido Oracle addresses, from which only report transactions will be accepted. You can get a list from Etherscan on [Hoodi](https://hoodi.etherscan.io/address/0x32EC59a78abaca3f91527aeB2008925D5AaC1eFC#readContract#F16) or [Mainnet](https://etherscan.io/address/0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288#readContract#F16) Format: ```json ["0x123", "0x123"] ``` ### MESSAGES_PASSWORD Password to decrypt encrypted exit messages with on app start. ### MESSAGES_PASSWORD_FILE Alternative to `MESSAGES_PASSWORD`. Path to a file with password inside to decrypt exit messages with. If used, MESSAGES_PASSWORD (not MESSAGES_PASSWORD_FILE) needs to be added to LOGGER_SECRETS in order to be sanitized ### BLOCKS_PRELOAD Amount of blocks to load events from on start. Suggested to include in your env variables, but to be left at default 50000 value (~7 days of blocks). In case your Ejector will be down due to an emergency, this value can be tweaked to let the Ejector load a higher amount of blocks on start. ### HTTP_PORT Port for serving metrics and a health check endpoint. Default is 8989. ### RUN_METRICS Enable with `true` to serve Prometheus metrics: [full list](https://github.com/lidofinance/validator-ejector#metrics). Will be served on `HOST:$HTTP_PORT/metrics`. Highly advised for monitoring and alerting. ### RUN_HEALTH_CHECK Enabled by default, disabled with `false`. Highly recommended to monitor this endpoint. Will be served on `HOST:$HTTP_PORT/health`. ### LOGGER_LEVEL Recommended to set to `info` (default), can be changed to `debug` in case of issues for easier debugging. ### LOGGER_FORMAT Format of logs, `simple` by default, but can be set to `json` to be easily parseable by [Loki](https://github.com/grafana/loki), for example. ### LOGGER_SECRETS Env var names or exact values which should be replaced in logs, in JSON array of strings format. Advised to include your MESSAGES_PASSWORD, EXECUTION_NODE, and MESSAGES_PASSWORD: ```json ["MESSAGES_PASSWORD", "EXECUTION_NODE", "CONSENSUS_NODE"] ``` Notice: make sure quotes are copied correctly if copying this sample. ### DRY_RUN Allows to test the app with `true` without actually sending out exit messages. Use with caution! Make sure to set to `false` or completely leave it out in production. ### Advanced Parameters Please don't use unless suggested by a Lido contributor. - BLOCKS_LOOP - 900 (3 hours of blocks) - Amount of blocks Ejector looks behind on wake in polling jobs. - JOB_INTERVAL - 384000 (1 epoch) - Time for which Ejector sleeps between jobs. - DISABLE_SECURITY_DONT_USE_IN_PRODUCTION - false - Set to `true` to skip security checks, for example if Exit Bus Consensus contract was changed after the Ejector was unable to exit validators eg was switched off. ## Running ### Source Code 1. Clone repository: ```bash git clone https://github.com/lidofinance/validator-ejector.git cd validator-ejector ``` 2. Create exit messages folder, for example locally `mkdir messages` 3. Put exit message files in the messages folder. 4. Copy env sample file `cp sample.env .env` 5. Fill environment variables in `.env` file. 6. Run ```bash yarn yarn build yarn start ``` ### Docker with docker-compose 1. Create root folder for Ejector, cd into that folder. 2. Create exit messages folder `mkdir messages` 3. Put exit message files in messages folder. 4. Copy env file `cp sample.env .env` 5. Fill environment variables in .env file. 6. Create `docker-compose.yml` file using the following template: https://github.com/lidofinance/validator-ejector/blob/develop/docker-compose.yml 6. Run `docker-compose up` or `docker-compose up -d` to start in detached mode (in background). ## Check Ejector is working 1. Ensure there are no errors in logs and no restarts. 2. Verify that config logged on start is correct in logs. 3. If you have put presigned messages in the messages folder, make sure Loaded Messages count is greater than `0`. 4. Ensure you can see `Job started` and `Job finished` lines in logs. Example of correct operation logs: ``` info: Application started, version 1.0.0 {"EXECUTION_NODE":"","CONSENSUS_NODE":"","LOCATOR_ADDRESS":"0x123","STAKING_MODULE_ID":"1","OPERATOR_ID":"0","MESSAGES_LOCATION":"messages","ORACLE_ADDRESSES_ALLOWLIST":["0x123"],"MESSAGES_PASSWORD":"","BLOCKS_PRELOAD":190000,"BLOCKS_LOOP":64,"JOB_INTERVAL":384000,"HTTP_PORT":8989,"RUN_METRICS":true,"RUN_HEALTH_CHECK":true,"DRY_RUN":false} info: Loading messages from messages info: Loaded 123 messages info: Validating messages info: Starting, searching only for requests for operator 0 info: Loading initial events for 190000 last blocks info: Job started {"operatorId":"0","stakingModuleId":"1","loadedMessages":123} info: Resolved Exit Bus contract address using the Locator {"exitBusAddress":"0x123"} info: Resolved Consensus contract address {"consensusAddress":"0x123"} info: Fetched the latest block from EL {"latestBlock":12345} info: Fetching request events from the Exit Bus {"eventsNumber":190000,"fromBlock":12345,"toBlock":12345} info: Loaded ValidatorExitRequest events {"amount":0} info: Handling ejection requests {"amount":0} info: Job finished info: Starting 384 seconds polling for 64 last blocks ``` ## What if something is wrong? 1. Make sure configuration is correct. 2. Make sure you are on the recommended Docker image SHA hash or version if running directly. 3. Check if Nodes are synced and are working correctly. 4. Restart the app. 5. Start the app with LOGGER_LEVEL=debug env variable and contact Lido devs with logs to investigate the problem. ## Additional Resources Validator Ejector GitHub Repository (Open Source) https://github.com/lidofinance/validator-ejector Lido Withdrawals: Automating Validator Exits - parts can now be outdated https://hackmd.io/@lido/BkxRxAr-o Ejector Logic Spec - parts can now be outdated https://hackmd.io/@lido/r1KZ4YNdj --- # Validator Exit Bot ## Introduction Validator Exit Bot automates triggering exits for Lido validators that have missed their exit deadline. The bot monitors the [ValidatorExitBusOracle](/guides/oracle-spec/validator-exit-bus) for exit requests, checks the current status of each validator on the beacon chain, and uses [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) triggerable exits to force the exit of any validator that has not exited on time. Exit trigger fees are paid from the bot's account and refunded by the withdrawal vault. ## Requirements ### Hardware - 1-core CPU - 1GB RAM ### Nodes - Ethereum EL RPC service - Ethereum CL API service (Beacon Node) ## How to use On startup the bot fetches historical `ExitDataProcessing` events from the `ValidatorExitBusOracle` for the configured lookback period. It then runs in a continuous loop: for each validator in its state, the bot checks whether the validator has already exited on the CL, and if not, whether it has missed its exit deadline. Validators past their deadline are included in a batched `trigger_exits` transaction. The bot's address is set as the refund recipient to recover the per-validator withdrawal request fee. ### Envs Required variables are (mainnet): | Variable | Default | Description | |---|---|---| | `WEB3_RPC_ENDPOINTS` | - | Comma-separated list of EL RPC endpoints | | `CONSENSUS_CLIENT_URL` | - | CL Beacon API endpoint | | `WALLET_PRIVATE_KEY` | - | Private key used to send transactions. Omit to run in dry mode (no transactions sent) | | `LIDO_LOCATOR` | `0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb` | Lido Locator address for Ethereum mainnet. Addresses for other supported networks can be found [here](/deployed-contracts/) | | `DRY_RUN` | `false` | If `true`, transactions are built but not submitted | Optional variables can be found [here](https://github.com/lidofinance/validator-exit-bot#readme). ## Running ### Source Code 1. Clone repository and install requirements: ```bash git clone git@github.com:lidofinance/validator-exit-bot.git cd validator-exit-bot ``` 2. Install requirements: ```bash poetry install ``` 3. Run validator exit bot: ```bash poetry run python -m src.main ``` ### Docker Docker image can be found [here](/guides/tooling/#validator-exit-bot). ## Monitoring Prometheus metrics will be available on endpoint `http://localhost:${PROMETHEUS_PORT}/metrics`. Health check endpoint: `http://localhost:${SERVER_PORT}/health`. --- # stETH on Optimism Parameters Validation ## Deployment scope The full list of the contracts in scope is provided in the Lido Multichain section for [Optimism](/deployed-contracts/#optimism). :::note Levers and access control lists for L1 and L2 bridge endpoints stay the same and correspond to the [reference architecture and permissions setup](/token-guides/cross-chain-tokens-guide.md#reference-wsteth-rollup-architecture-and-permissions-setup). This document covers incremental changes proposed with the stETH on Optimism deployment. ::: ## Ethereum part ### LidoLocator :::info A new implementation containing the address of a new `TokenRateNotifier` instance for [`postTokenRebaseReceiver`](/contracts/lido-locator/#posttokenrebasereceiver) ::: ### TokenRateNotifier :::info A standalone (non-proxy) contract proposed to plug to the protocol as a new [token rebase receiver](/contracts/lido-locator/#posttokenrebasereceiver). The contract maintains token rate observers that needs to be notified about token rate changes. With the current setup the only single observer is connected upfront: the `OpStackTokenRatePusher` contract instance for pushing the token rate to Optimism. ::: ```bash # Account that is allowed to add or remove pushers. # set to the be the Lido DAO Agent governance contract owner=0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c # Address of Lido Core protocol contract # corresponds to the stETH token address on Mainnet LIDO=0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 # Max amount of token rate observers that can be added inside this pusher MAX_OBSERVERS_COUNT=32 # Current length of observers observersLength=1 # The initially added `OpStackTokenRatePusher` observer for Optimism # see https://docs.lido.fi/deployed-contracts/#optimism observers[0]=0xd54c1c6413caac3477AC14b2a80D5398E3c32FfE ``` ### OpStackTokenRatePusher :::info A standalone (non-proxy) contract to be a receiver for the `TokenRateNotifier` contract above, and allowing to push an up-to-date wstETH/stETH [token rate](/contracts/wsteth/#stethpertoken) as a part of the `AccountingOracle` report [processing](/contracts/accounting-oracle/#report-processing). The rate gets pushed to an `TokenRateOracle` instance on Optimism. ::: ```bash # Address of the non-rebasable token (wstETH) on L1 to deploy the bridge/gateway for. WSTETH=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 # Precision for sending stETH / wstETH token rate # i.e.: `wsteth.getStETHByWstETH(10 ** TOKEN_RATE_DECIMALS)` TOKEN_RATE_DECIMALS=27 # The address of the `TokenRateOracle` contract on Optimism L2_TOKEN_RATE_ORACLE=0x294ED1f214F4e0ecAE31C3Eae4F04EBB3b36C9d0 # Beacon Chain (Consensus Layer) genesis timestamp GENESIS_TIME=1606824023 # Seconds per a single slot according to the Ethereum CL spec SECONDS_PER_SLOT=12 # Address of the AccountingOracle of the Lido protocol. ACCOUNTING_ORACLE=0x852deD011285fe67063a08005c71a85690503Cee # @notice Gas limit for L2 required to finish pushing token rate on L2 side. # Client pays for gas on L2 by burning it on L1. # Depends linearly on deposit data length and gas used for finalizing deposit on L2. # Formula to find value: # (gas cost of `L2Bridge.finalizeDeposit() + OptimismPortal.minimumGasLimit(depositData.length)) * 1.5` L2_GAS_LIMIT_FOR_PUSHING_TOKEN_RATE=300000 ``` ### L1LidoTokensBridge :::info A new implementation for the previously deployed `L1ERC20TokenBridge` L1 endpoint proxy contract to be compatible with both wstETH and stETH tokens bridging flows, escrows wstETH on the Ethereum side. ::: ```bash # Address of the L2 token bridge counterpart proxy endpoint for stETH and wstETH. l2TokenBridge=0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957 # Address of the non-rebasable token (L1 wstETH) to deploy the bridge/gateway for. L1_TOKEN_NON_REBASABLE=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 # Address of L1 wstETH (the same as L1_NON_REBASABLE_TOKEN) WSTETH=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 # Address of the rebasable token (L1 stETH) to deploy the bridge/gateway for. L1_TOKEN_REBASABLE=0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 # Address of the L2 non-rebasable token (L2 wstETH) proxy on L2. L2_TOKEN_NON_REBASABLE=0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb # Address of the L2 rebasable token (L2 stETH) proxy on L2. L2_TOKEN_REBASABLE=0x76A50b8c7349cCDDb7578c6627e79b5d99D24138 # Optimism messenger contract `L1CrossDomainMessengerProxy` # https://docs.optimism.io/chain/addresses#ethereum-l1 MESSENGER=0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1 # Precision for sending stETH / wstETH token rate # i.e.: `wsteth.getStETHByWstETH(10 ** TOKEN_RATE_DECIMALS)` TOKEN_RATE_DECIMALS=27 # Beacon Chain (Consensus Layer) genesis timestamp GENESIS_TIME=1606824023 # Seconds per a single slot according to the Ethereum CL spec SECONDS_PER_SLOT=12 # Address of the AccountingOracle of Core Lido protocol. ACCOUNTING_ORACLE=0x852deD011285fe67063a08005c71a85690503Cee ``` ## Optimism part ### TokenRateOracle :::info A source of truth contract (proxy and implementation) for wstETH/stETH token rate on the Optimism side. Used for `wrap` and `unwrap` on Optimism side, including wstETH internal bookkeeping when bridging stETH back and forth. ::: ```bash # Precision for receiving and storing stETH / wstETH token rate DECIMALS=27 # Address of the L1 token rate pusher (OpStackTokenRatePusher) instance L1_TOKEN_RATE_PUSHER=0xd54c1c6413caac3477AC14b2a80D5398E3c32FfE # Address of L2 token bridge proxy. L2_ERC20_TOKEN_BRIDGE=0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957 # Optimism messenger contract `L2CrossDomainMessenger` # https://docs.optimism.io/chain/addresses#op-mainnet-l2 MESSENGER=0x4200000000000000000000000000000000000007 # A time period when token rate can be considered outdated. TOKEN_RATE_OUTDATED_DELAY=86400 # 1 day due to the regular stETH token rebase cadence # A time difference between received l1Timestamp and L2 block.timestamp when token rate can be considered outdated. MAX_ALLOWED_L2_TO_L1_CLOCK_LAG=86400 # 1 day # Allowed token rate deviation per day in basis points. MAX_ALLOWED_TOKEN_RATE_DEVIATION_PER_DAY_BP=500 # 500 BP = 5% # The maximum allowed time difference between the current time and the last received # token rate update that can be set during a pause. This is required to limit the pause role # and mitigate potential economic attacks. OLDEST_RATE_ALLOWED_IN_PAUSE_TIME_SPAN=86400 # 1 day # The maximum delta time that is allowed between two L1 timestamps of token rate updates. MIN_TIME_BETWEEN_TOKEN_RATE_UPDATES=3600 # 1 hour # Minimal sane token rate to accept (sanity check) MIN_SANE_TOKEN_RATE=10000000000000000000000000 # 0.01 * 10^27 # Maximal sane token rate to accept (sanity check) MAX_SANE_TOKEN_RATE=100000000000000000000000000000 # 100 * 10^27 # Enable token rate oracle updates TOKEN_RATE_UPDATE_ENABLED=true # Roles granting the permission to resume updating rate. # Optimism Governance Bridge Executor TOKEN_RATE_UPDATE_ENABLERS=["0xefa0db536d2c8089685630fafe88cf7805966fc3"] # Roles granting the permission to pause updating rate. # Optimism Governance Bridge Executor # Emergency brakes committee TOKEN_RATE_UPDATE_DISABLERS=["0xefa0db536d2c8089685630fafe88cf7805966fc3", "0x4Cf8fE0A4c2539F7EFDD2047d8A5D46F14613088"] # Address of the account to grant the DEFAULT_ADMIN_ROLE # Optimism Governance Bridge Executor TOKEN_RATE_ORACLE_ADMIN=0xefa0db536d2c8089685630fafe88cf7805966fc3 ``` ### ERC20BridgedPermit for wstETH :::info A new implementation for the wstETH (non-rebasable) token deployed on Optimism. Provides a way to seamlessly wrap/unwrap to stETH and implements new permit signature standards (ERC-2612 and ERC-1271). ::: ```bash # Bridge address that can mint/burn wstETH on L2 bridge=0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957 # Token name for wstETH on Optimism name="Wrapped liquid staked Ether 2.0" # Token symbol for wstETH on Optimism symbol="wstETH" # Token decimals decimals=18 ``` ### ERC20RebasableBridgedPermit for stETH :::info An ERC-20 compatible contract (proxy and implementation) for the rebasable stETH token deployed on Optimism. Provides a way to seamlessly wrap/unwrap from wstETH and implements permit signature standards (ERC-2612 and ERC-1271). ::: ```bash # Bridge address that can mint/burn wstETH on L2 L2_ERC20_TOKEN_BRIDGE=0x8E01013243a96601a86eb3153F0d9Fa4fbFb6957 # Token rate oracle on Optimism for wstETH/stETH token rate retrieval upon wrap and unwrap TOKEN_RATE_ORACLE=0x294ED1f214F4e0ecAE31C3Eae4F04EBB3b36C9d0 # Token rate decimals precision used by the token rate oracle on Optimism TOKEN_RATE_ORACLE_DECIMALS=27 # wstETH on Optimism address # stETH on Optimism is 'wrapped' wstETH technically TOKEN_TO_WRAP_FROM=0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb # Token name for stETH on Optimism name="Liquid staked Ether 2.0" # Token symbol for stETH on Optimism symbol="stETH" # Token decimals decimals=18 ``` ### L2ERC20ExtendedTokensBridge :::info A new implementation for the previously deployed `L2ERC20TokenBridge` L2 endpoint proxy contract to be compatible with both wstETH and stETH tokens bridging flows, mints and burns wstETH on the Optimism side. ::: ```bash # Address of L1 token bridge proxy. l1TokenBridge=0x76943C0D61395d8F2edF9060e1533529cAe05dE6 # Address of the non-rebasable token (L1 wstETH) to deploy the bridge/gateway for. L1_TOKEN_NON_REBASABLE=0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 # Address of the rebasable token (L1 stETH) to deploy the bridge/gateway for. L1_TOKEN_REBASABLE=0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 # Address of the L2 non-rebasable token (L2 wstETH) proxy on L2. L2_TOKEN_NON_REBASABLE=0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb # Address of the L2 rebasable token (L2 stETH) proxy on L2. L2_TOKEN_REBASABLE=0x76A50b8c7349cCDDb7578c6627e79b5d99D24138 # Optimism messenger contract `L2CrossDomainMessenger` # https://docs.optimism.io/chain/addresses#op-mainnet-l2 MESSENGER=0x4200000000000000000000000000000000000007 ``` --- # Lido Council Daemon [Lido Council Daemon](https://github.com/lidofinance/lido-council-daemon) is the off-chain service run by every member of the Deposit Security Committee (DSC). It monitors validator public keys in the `DepositContract` and across all Lido staking modules, signs `(depositRoot, keysOpIndex)` messages with the guardian's EOA key to permit deposits, and โ€” if a malicious pre-deposit is detected โ€” broadcasts a `pause` message that any single guardian can use to halt deposits, providing one-honest-participant safety against supermajority collusion. For the full description of the threat model, daemon configuration and on-chain interactions, see the [Deposit Security Committee manual](/guides/deposit-security-manual) and the [`DepositSecurityModule`](/contracts/deposit-security-module) contract reference. The committee was originally formed as part of [LIP-5: Mitigations for deposit front-running vulnerability](https://research.lido.fi/t/mitigations-for-deposit-front-running-vulnerability/1239); funding for guardian infrastructure is tracked in the [Council Daemons Funding](https://research.lido.fi/t/council-daemons-funding/8526) thread. ## Mainnet members \[[proposed to rotate](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746)\] Current members: | Operator | Address | Announcement | |--------------------|-------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| | Blockscape | [`0x7912Fa976BcDe9c2cf728e213e892AD7588E6AaF`](https://etherscan.io/address/0x7912Fa976BcDe9c2cf728e213e892AD7588E6AaF) | [post on x.com](https://x.com/BlockscapeLab/status/1452902878885068803) | | Kiln | [`0x6d22aE126eB2c37F67a1391B37FF4f2863e61389`](https://etherscan.io/address/0x6d22aE126eB2c37F67a1391B37FF4f2863e61389) | [post on x.com](https://x.com/Kiln_finance/status/1969039576170745945), [exit request](https://research.lido.fi/t/kiln-requesting-to-exit-the-lido-deposit-security-committee/11813) | | Staking Facilities | [`0xf82D88217C249297C6037BA77CE34b3d8a90ab43`](https://etherscan.io/address/0xf82D88217C249297C6037BA77CE34b3d8a90ab43) | [post on x.com](https://x.com/StakingFac/status/1452656210927394818) | | Lido dev team | [`0x5fd0dDbC3351d009eb3f88DE7Cd081a614C519F1`](https://etherscan.io/address/0x5fd0dDbC3351d009eb3f88DE7Cd081a614C519F1) | [post on x.com](https://x.com/LidoFinance/status/1452973085557149709) | | P2P | [`0xa56b128Ea2Ea237052b0fA2a96a387C0E43157d8`](https://etherscan.io/address/0xa56b128Ea2Ea237052b0fA2a96a387C0E43157d8) | [post on x.com](https://x.com/P2Pvalidator/status/1452970276480819208) | | Stakefish | [`0x4B87F16B8d32cb5a859a4C48a88edB5adBe3498E`](https://etherscan.io/address/0x4B87F16B8d32cb5a859a4C48a88edB5adBe3498E) | [post on x.com](https://x.com/stakefish/status/2041526695866323442) | \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746)\] EDF DelegationContracts (LIP-37). Kiln leaves the committee and Stakely takes its seat, see the [Kiln exit request](https://research.lido.fi/t/kiln-requesting-to-exit-the-lido-deposit-security-committee/11813): | Operator | Address | Announcement | |--------------------|-------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| | Blockscape | [`0xDc1579636686C082fc3b00B9EB25259A110D0C44`](https://etherscan.io/address/0xDc1579636686C082fc3b00B9EB25259A110D0C44) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/11) | | Stakely | [`0x6A22d74a816662078f2371A7138E7614874cd61d`](https://etherscan.io/address/0x6A22d74a816662078f2371A7138E7614874cd61d) | [replacing Kiln](https://research.lido.fi/t/kiln-requesting-to-exit-the-lido-deposit-security-committee/11813/3), [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/12) | | Staking Facilities | [`0x35506190Ca6df385aA6Bc4a970646dd4f49426c1`](https://etherscan.io/address/0x35506190Ca6df385aA6Bc4a970646dd4f49426c1) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/13) | | Lido dev team | [`0x915F0Fa50E1af761B113b41c79ab33Bf4734C36E`](https://etherscan.io/address/0x915F0Fa50E1af761B113b41c79ab33Bf4734C36E) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/21) | | P2P | [`0xe387Ba1d5C9f6306eCe9ac949C7fB6233dD5411E`](https://etherscan.io/address/0xe387Ba1d5C9f6306eCe9ac949C7fB6233dD5411E) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/16) | | Stakefish | [`0x031E597BcF680f1f2293b119b4b2B14096B15497`](https://etherscan.io/address/0x031E597BcF680f1f2293b119b4b2B14096B15497) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/15) | **Signing quorum:** - **Deposit intent** โ€” 4 out of 6 guardian signatures are required to permit a deposit. The current value is enforced on-chain and is readable via [`getGuardianQuorum()`](/contracts/deposit-security-module#getguardianquorum) on the [`DepositSecurityModule`](/contracts/deposit-security-module). - **Pause** โ€” a single guardian signature is sufficient to pause deposits via [`pauseDeposits()`](/contracts/deposit-security-module#pausedeposits). This gives any one honest member the ability to halt further deposits even if the rest of the committee colludes. --- # Lido Oracle [Lido Oracle](https://github.com/lidofinance/lido-oracle) is the off-chain daemon, run independently by each oracle member, that bridges Ethereum's Consensus and Execution layers for the Lido protocol. Members build reports off-chain and reach consensus on them via the on-chain `HashConsensus` contract; the agreed-upon report is then submitted on-chain and applied by Lido contracts. For a deep technical overview, see the [Oracle Operator Manual](/guides/oracle-operator-manual) and the [Oracle specification](/guides/oracle-spec/accounting-oracle). Onboarding of new oracle members and rotation of existing members is coordinated on the research forum in the [Expansion of Lido's Ethereum Oracle set](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836) thread. ## Mainnet members \[[proposed to rotate](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746)\] Current members: | Operator | Address | Forum post | |----------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Chorus One (Bitwise) | [`0x8dB977C13CAA938BC58464bFD622DF0570564b78`](https://etherscan.io/address/0x8dB977C13CAA938BC58464bFD622DF0570564b78) | [address rotation](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/80) | | Staking Facilities | [`0x404335BcE530400a5814375E7Ec1FB55fAff3eA2`](https://etherscan.io/address/0x404335BcE530400a5814375E7Ec1FB55fAff3eA2) | [post on x.com](https://x.com/StakingFac/status/1346557400057327616), [Lido post](https://x.com/LidoFinance/status/1346555154007470088) | | Stakefish | [`0x042a9e5acCfa17e28300F1b5967f20891E973922`](https://etherscan.io/address/0x042a9e5acCfa17e28300F1b5967f20891E973922) | [address rotation](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/81) | | P2P | [`0x007DE4a5F7bc37E2F26c0cb2E8A95006EE9B89b5`](https://etherscan.io/address/0x007DE4a5F7bc37E2F26c0cb2E8A95006EE9B89b5) | [address confirmation](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/84) | | bloXroute | [`0x61c91ECd902EB56e314bB2D5c5C07785444Ea1c8`](https://etherscan.io/address/0x61c91ECd902EB56e314bB2D5c5C07785444Ea1c8) | [intent to join](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/29), [address declaration](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/54) | | Instadapp | [`0x73181107c8D9ED4ce0bbeF7A0b4ccf3320C41d12`](https://etherscan.io/address/0x73181107c8D9ED4ce0bbeF7A0b4ccf3320C41d12) | [intent to join](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/30), [address declaration](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/71) | | Chainlayer | [`0xc79F702202E3A6B0B6310B537E786B9ACAA19BAf`](https://etherscan.io/address/0xc79F702202E3A6B0B6310B537E786B9ACAA19BAf) | [intent to join](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/17), [replacing Jump Crypto](https://research.lido.fi/t/jump-crypto-replacement-in-the-oracle-set/5620/8) | | MatrixedLink | [`0xe57B3792aDCc5da47EF4fF588883F0ee0c9835C9`](https://etherscan.io/address/0xe57B3792aDCc5da47EF4fF588883F0ee0c9835C9) | [intent to join (replacing Rated)](https://research.lido.fi/t/rated-labs-replacement-in-the-oracle-set/7850/2), [address declaration](https://research.lido.fi/t/rated-labs-replacement-in-the-oracle-set/7850/14) | | Caliber | [`0x4118DAD7f348A4063bD15786c299De2f3B1333F3`](https://etherscan.io/address/0x4118DAD7f348A4063bD15786c299De2f3B1333F3) | [intent to join (replacing Kyber)](https://research.lido.fi/t/expansion-of-lidos-ethereum-oracle-set/2836/78) | \[[proposed](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746)\] EDF DelegationContracts (LIP-37): | Operator | Address | Forum post | |----------------------|-------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| | Chorus One (Bitwise) | [`0x56B3eA8016Da18C6E8CD8135492d242F0dE0DBBC`](https://etherscan.io/address/0x56B3eA8016Da18C6E8CD8135492d242F0dE0DBBC) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/22) | | Staking Facilities | [`0xc7442d4d8F3FfEa0fA4a18Ad3062c8137cE21749`](https://etherscan.io/address/0xc7442d4d8F3FfEa0fA4a18Ad3062c8137cE21749) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/13) | | Stakefish | [`0x5e8Ed9f10307eD6FA793A347e4D0f407D00B9C6f`](https://etherscan.io/address/0x5e8Ed9f10307eD6FA793A347e4D0f407D00B9C6f) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/15) | | P2P | [`0x4E3F2DEeb59eB9a205D82D17647b3e56422e0FEe`](https://etherscan.io/address/0x4E3F2DEeb59eB9a205D82D17647b3e56422e0FEe) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/17) | | bloXroute | [`0x99Cd2EF33040879D40BBC77Df81863D97f13C64d`](https://etherscan.io/address/0x99Cd2EF33040879D40BBC77Df81863D97f13C64d) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/23) | | Instadapp | [`0xE75A431A98487DC69A14Bdd13d858E3238e9C1b3`](https://etherscan.io/address/0xE75A431A98487DC69A14Bdd13d858E3238e9C1b3) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/20) | | Chainlayer | [`0xd524101C3c40f71Fce7B9312D299603880a06Bdb`](https://etherscan.io/address/0xd524101C3c40f71Fce7B9312D299603880a06Bdb) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/14) | | MatrixedLink | [`0xC4f2704273598d51A0ec76A31C12553ec8f5A891`](https://etherscan.io/address/0xC4f2704273598d51A0ec76A31C12553ec8f5A891) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/19) | | Caliber | [`0xc77d0Bf3AA4778E36a89CDC8bbc9c34d8060637d`](https://etherscan.io/address/0xc77d0Bf3AA4778E36a89CDC8bbc9c34d8060637d) | [EDF DelegationContract](https://research.lido.fi/t/lip-37-execution-delegation-framework-edf/11746/18) | **Consensus quorum:** 5 out of 9 identical report hashes are required to finalize a report. The current value is enforced on-chain and is readable via [`getQuorum()`](/contracts/hash-consensus#getquorum) on each `HashConsensus` instance. The authoritative on-chain set is maintained by the [`HashConsensus`](/contracts/hash-consensus) contract; the values above can be cross-checked via [`getMembers()`](/contracts/hash-consensus#getmembers) on the `HashConsensus` instances bound to the [`AccountingOracle`](/contracts/accounting-oracle), [`ValidatorsExitBusOracle`](/contracts/validators-exit-bus-oracle) and [`FeeOracle`](/staking-modules/csm/contracts/FeeOracle). --- # Legacy Aave V2 stETH integration :::warning V2 only This notice applies only to Aave V2's direct integration with rebasable stETH. Do not use that legacy market for new positions or integrations. ::: Aave V3 and Aave V4 integrate wstETH as collateral. Aave V3 includes a [dedicated Lido market](https://aave.com/blog/lido-aave-case-study), and Aave V4 includes wstETH in its [main and dedicated Lido configurations](https://governance.aave.com/t/arfc-aave-v4-activation-on-ethereum-mainnet/24293). These wstETH integrations are separate from the deprecated V2 stETH integration and are not deprecated by this notice. Aave moved V2 out of its main interface while preserving a [legacy V2 interface](https://v2-market.aave.com/) for users with existing positions to repay, withdraw, or migrate. See [Aave's V2 interface deprecation notice](https://governance.aave.com/t/aave-v2-interface-deprecation/23335) for details. The operational documentation is retired, but the integration's durable design lesson remains documented in [stETH vs. wstETH](/guides/lido-tokens-integration-guide#aave-v2-integration-lesson). For current integrations, use [wstETH on Aave](/guides/lido-tokens-integration-guide#sttokens-steth-and-wsteth). Historical Aave V2 contract addresses remain available in [deployed contracts](/deployed-contracts/#aave-v2-integration). --- # API :::info Lido APIs are strictly for read-only access ::: Here you can find various Lido APIs which you can integrate in your app or website: ## Lido APR API provides Ethereum and Lido staking APR, which include: ### Simple Moving Average Lido APR for 7 last days: This APR value is based on Simple Moving Average of APR values over a period of 7 days. ``` https://eth-api.lido.fi/v1/protocol/steth/apr/sma ``` Response schema and examples are available in the [Swagger API documentation](https://eth-api.lido.fi/api/#/APR%20for%20Eth%20and%20stEth/ProtocolController_findSmaAPRforSTETH) ### Hoodi ``` https://eth-api-hoodi.testnet.fi/v1/protocol/steth/apr/sma ``` ### Last Lido APR for stETH The latest staking APR value. For legacy deployments, APR values were collected by periodically fetching oracle report events. For Lido V2+ the value is calculated based on [rebase events](https://github.com/lidofinance/core/blob/v4.0.0/contracts/0.4.24/Lido.sol#L210-L218) using the following algorithm: ```solidity // Emits when token rebased (total supply and/or total shares were changed) event TokenRebased( uint256 indexed reportTimestamp, uint256 timeElapsed, uint256 preTotalShares, uint256 preTotalEther, /* preTotalPooledEther */ uint256 postTotalShares, uint256 postTotalEther, /* postTotalPooledEther */ uint256 sharesMintedAsFees /* fee part included in `postTotalShares` */ ); preShareRate = preTotalEther * 1e27 / preTotalShares postShareRate = postTotalEther * 1e27 / postTotalShares userAPR = secondsInYear * ( (postShareRate - preShareRate) / preShareRate ) / timeElapsed ``` ``` https://eth-api.lido.fi/v1/protocol/steth/apr/last ``` Response schema and examples are available in the [Swagger API documentation](https://eth-api.lido.fi/api/#/APR%20for%20Eth%20and%20stEth/ProtocolController_findLastAPRforSTETH) #### Hoodi ``` https://eth-api-hoodi.testnet.fi/v1/protocol/steth/apr/last ``` ## Lido Reward History Reward History Backend provides an API which returns all stETH interactions by an address and calculates its daily stETH rewards. Currently, there's just one endpoint (`/`): ``` https://reward-history-backend.lido.fi/?address=0x12345 ``` Response schema and examples are available in the [Swagger API documentation](https://reward-history-backend.lido.fi/api) ### Parameters The only required query parameter is `address`. Optional Parameters: - `currency`: USD/EUR/GBP - Fiat currency in which to display stETH denominated in fiat. **USD** by default. - `archiveRate`: true/false - Use an exchange rate close to the transaction time when calculating currency values instead of the current one. **true** by default. - `onlyRewards`: true/false - Include only rewards without transfers or stakings. **false** by default. - `sort`: asc/desc - Sort of transactions by blockTime. **desc** by default. - `skip`: number - Amount of data items to skip. - `limit`: number - Maximum amount of data items to respond with. `skip` and `limit` params are used for pagination, e.g.: ``` skip: 0, limit: 100 = 1 page skip: 100, limit: 100 = 2 page skip: 200, limit: 100 = 3 page ``` ### Hoodi Reward History Backend is also available on Hoodi testnet: ``` http://reward-history-backend-hoodi.testnet.fi/?address=0x12345 ``` Response schema and examples are available in the [Swagger API documentation](https://reward-history-backend-hoodi.testnet.fi/api) ## Withdrawals API The Withdrawals API service offers an utility for estimating the waiting time for [withdrawals](/contracts/withdrawal-queue-erc721) within the Lido on Ethereum protocol. The service is helpful for stakers, providing insights from the moment of withdrawal request placement to its finalization when the request becomes claimable. See the [detailed explanation](https://github.com/lidofinance/withdrawals-api/blob/develop/how-estimation-works.md). ### Use Cases - Estimation before request: users can estimate the waiting time before placing a withdrawal request. - Tracking the existing request: users can track the estimated waiting time for the already placed request. ### Calculates time to withdrawals requests: ``` https://wq-api.lido.fi/v2/request-time?ids=1&ids=2 ``` Response schema and examples are available in the [Swagger API documentation](https://wq-api.lido.fi/api#/Request%20Time/RequestTimeController_requestsTime) ### Calculate time to withdrawal current queue: ``` https://wq-api.lido.fi/v2/request-time/calculate ``` ### Calculates time to withdrawal amount of stETH: ``` https://wq-api.lido.fi/v2/request-time/calculate?amount=32 ``` Response schema and examples are available in the [Swagger API documentation](https://wq-api.lido.fi/api#/Request%20Time/RequestTimeController_calculateTime) ### Hoodi ``` https://wq-api-hoodi.testnet.fi/v2/request-time?ids=1&ids=2 ``` Response schema and examples are available in the [Swagger API documentation](https://wq-api-hoodi.testnet.fi/api#/Request%20Time/RequestTimeController_requestsTime) --- # SDKs and UI libraries ### Lido UI Library **Target clients**: those who want to use Lido UI components in their projects. React components for Lido Finance projects. - Storybook: [https://ui.lido.fi](https://ui.lido.fi/) - GitHub: [https://github.com/lidofinance/ui](https://github.com/lidofinance/ui) - NPM: [https://www.npmjs.com/search?q=%40lidofinance/](https://www.npmjs.com/search?q=%40lidofinance/) ### Lido Ethereum SDK **Target clients**: those who want to interact/integrate with the Lido protocol in JavaScript/TypeScript projects. Library for interaction with the Lido on Ethereum protocol. - Documentation: [https://lidofinance.github.io/lido-ethereum-sdk/](https://lidofinance.github.io/lido-ethereum-sdk/) - GitHub: [https://github.com/lidofinance/lido-ethereum-sdk](https://github.com/lidofinance/lido-ethereum-sdk) - NPM: [https://www.npmjs.com/package/@lidofinance/lido-ethereum-sdk](https://www.npmjs.com/package/@lidofinance/lido-ethereum-sdk) --- # Subgraph ## Introduction Lido has a Subgraph deployed on [The Graph Decentralized Network](https://thegraph.com/docs/about/introduction#what-the-graph-is) which indexes and organises data from the Lido smart contracts events, exposing a GraphQL endpoint for queries. Subgraph data is indexed and served by independent Indexers on the network. ## GraphQL Schema The schema of GraphQL entities available is defined in [`/schema.graphql` ](https://github.com/lidofinance/lido-subgraph/blob/master/schema.graphql). ## Links - [Explorer Page](https://thegraph.com/explorer/subgraph?id=Sxx812XgeKyzQPaBpR5YZWmGV5fZuBaPdh7DFhzSwiQ&view=Overview) - GraphQL Endpoint: `https://gateway-arbitrum.network.thegraph.com/api/[api-key]/subgraphs/id/Sxx812XgeKyzQPaBpR5YZWmGV5fZuBaPdh7DFhzSwiQ` - [Code Repo](https://github.com/lidofinance/lido-subgraph/) ## Query Examples Below are some sample queries you can use to gather information from the Lido contracts. You can build your own queries using [GraphQL Explorer](https://graphiql-online.com) to test it out and query exactly what you need. ### Rewards Distribution Daily staking rewards data with calculated APR and fees distribution. ```graphql { totalRewards(first: 100, orderBy: block, orderDirection: desc) { id totalRewards totalRewardsWithFees insuranceFee treasuryFee totalFee dust nodeOperatorFees { address fee } nodeOperatorsShares { address shares } shares2mint sharesToInsuranceFund sharesToOperators sharesToTreasury totalPooledEtherBefore totalPooledEtherAfter totalSharesBefore totalSharesAfter apr aprBeforeFees aprRaw preTotalPooledEther postTotalPooledEther timeElapsed block blockTime transactionIndex } } ``` ### Oracle Reports Daily completed oracle reports. ```graphql { oracleCompleteds(first: 500, orderBy: blockTime, orderDirection: desc) { epochId beaconBalance beaconValidators block blockTime } } ``` ### Transfers stETH transfers between addresses. ```graphql { lidoTransfers(first: 50) { from to value block blockTime transactionHash } } ``` ### Submissions stETH staking events. ```graphql { lidoSubmissions(first: 50) { sender amount block blockTime transactionHash } } ``` ### Node Operator Keys Fetch validator keys of a node operator. ```graphql { nodeOperatorSigningKeys(where: { operatorId: 0 }) { pubkey } } ``` ## Helpful Links [Creating an API Key Video Tutorial](https://www.youtube.com/watch?v=UrfIpm-Vlgs) [Managing your API Key & Setting your indexer preferences](https://thegraph.com/docs/en/studio/managing-api-keys/) [Querying from an Application](https://thegraph.com/docs/en/developer/querying-from-your-app/) --- # Wallets By integrating Lido staking into your app or website you may be eligible for [Lido Rewards-Share Program](https://research.lido.fi/t/rewards-share-program-2024/6812). *To participate in [Lido Rewards-Share Program](https://research.lido.fi/t/rewards-share-program-2024/6812), file your application following the onboarding process described.* ## Your referral link You can start referring people simply by sharing your special referral link to Lido [Staking Widget](https://stake.lido.fi/). Your referral link looks like this `https://stake.lido.fi/?ref=YOUR_REWARDS_ADDRESS` where `YOUR_REWARDS_ADDRESS` is your Ethereum address or ENS name registered for the Lido Referral Program. Anyone who uses your link to stake will automatically set your rewards address as their referral. For example, Vitalik's referral link would be [`https://stake.lido.fi/?ref=0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B`](https://stake.lido.fi/?ref=0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B). ## Integration options There are multiple ways of integrating Lido staking into your app or website. ### Adding a banner You can add a banner to your website that redirects the user to Lido Staking Widget using your referral link. We have [several SVG banners](https://github.com/lidofinance/referral-program-integration-examples/tree/main/banners) for you to choose from. Example code, ```html ``` Preview, For more examples, click [here](https://github.com/lidofinance/referral-program-integration-examples/tree/main/examples/banners). ### Embedding Staking Widget If you don't want users to leave your app to stake, you can embed Staking Widget via the `iframe` HTML tag and use your referral link as the source. Example code, ```html ``` ### Show only specific vaults This example limits the visible vaults to a specific subset (for example `eth` and `usd`). All other vaults are hidden. ```html ``` ### Link to a specific vault This example links directly to the deposit page of a specific vault (for example `eth`). ```html ``` --- # Referral tracking MetaVaults deposits can include additional parameters embedded directly in the onchain transaction, enabling reliable attribution and access control without relying on offchain tracking. ## Deposit parameters The following optional fields may be provided during deposit: - **assets**: The quantity of tokens being deposited, expressed in the token's smallest unit. For example, a deposit of 1 wstETH is represented as `1e18`. - **referral**: A tracking code used to attribute the source of the deposit (for example partner, campaign, or distribution channel). - **merkleProof**: A proof used to validate whitelisted deposits, ensuring that only approved addresses or allocations can participate when whitelist restrictions are enabled. These parameters are written directly into the deposit queue contract and become part of the onchain record for the transaction. For the exact implementation and parameter structure, see the contract source: [DepositQueue.sol](https://github.com/mellow-finance/flexible-vaults/blob/main/src/queues/DepositQueue.sol#L64). ## Recommended integration approach For accurate attribution, it is strongly recommended to use a custom UI or integration layer that: - Injects the correct referral code into the deposit transaction. - Ensures parameters are consistently formatted and recorded onchain. Because the data is embedded in the transaction itself, this method provides deterministic and verifiable attribution. ## How to integrate the referral address You can set up referral attribution by sharing a vault link with a personalized web parameter that includes your wallet address as the referral ID. 1. Copy your wallet address. This address acts as your unique referral identifier. 2. Add it to the vault link by attaching `?ref=YOUR_WALLET_ADDRESS`. 3. Share the link. When someone opens your link and deposits through the interface, your wallet address is recorded onchain as the referral source. Example: ``` https://stake.lido.fi/earn/eth/deposit?ref=YOUR_WALLET_ADDRESS ``` ## Limitations of URL-based referrals Web referrals passed via URL parameters (for example `?ref={code}`) are technically possible but not fully reliable in the crypto space. Many users interact through: - Privacy-focused browsers - Wallet in-app browsers - Direct contract interactions These flows often strip or ignore URL query parameters, leading to incomplete or incorrect attribution.