Oracle daemon (further Oracle) for Lido stacking protocol collects, make some decisions and reports Consensus and Execution Layer data to Lido Oracle smart contract. Depends in which mode Oracle is running it reports validator's balances on Consensus Layer, skimmed rewards, withdrawal requests and validators to eject.
Validators ejection works separately from main oracle report. It could happen more often, depends on Oracle contract setup and withdrawals activity.
New Oracle will contain 3 modules:
Oracle will build the report using first non-missed slot in last finalized epoch, that multiple of “epoch per frame”.
Oracle smart contract could be in 3 states:
(this also applies to exit bus smart contract)
Ready for report
Oracle is ready to receive report's hash from the participants of committee.
handleCommitteeMemberReport(epochId, reportHash) method.
Ready to finalize
Oracle got enough hashes and the report is ready to be finalized.
One of the committee should send report data (hash of which was used above).
handleReportData(reportData) method.
Behavior could be changed in this part. Sending report's data can take place in several stages.
Reported
All calculations are done and Oracle is inactive until new epoch in next frame will be finalized.
Oracle could work in two modes:
Details about bunker mode are here.
There are quite a few places in the Oracle where we try to predict the income to vault for a certain number of blocks.
To do this we need to calculate income for equal periods of time before the period of time for which we want to know the income and calculate the median of these values.
For example, we want to predict income to EL vault for 7200 blocks (1 day). We will check income for past 10 days day by day. And the median of that values will be our assumption income for next day.
Update path:
There are two different types of update.
Major
Minor
Minor updates could be done totally asynchronously.
Major update won't work until >50% of Oracle will be updated.
There are two ways to do Major updates:
Each module is a statless, independent unit that could be used in a separate service.
There is one entrypoint in Oracle daemon that contain all modules.
For every new finalized epoch, it fetches the slot and block number and calls run_module method in each module.
LidoOracle smart contract source code.
Field name | Type | Description |
---|---|---|
beaconValidators | uint256 | Count all Lido's validators on Consensus Layer |
beaconBalanceGwei | uint64 | Sum all Lido's validators balances on Consensus Layer |
stakingModules | address[] | Source of Node Operator |
nodeOperatorsWithExitedValidators | uint256[] | Node Operator ID in contract |
exitedValidatorsNumbers | uint256[] | Count of exited validators for each Node Operator |
withdrawalVaultBalance | uint256 | WithdrawalCredentionals vault balance |
withdrawalsReserveAmount | uint256 | Amount of deposit buffer to hold. Will be used to fulfill withdrawal request in next Oracle's iteration. |
requestIdToFinalizeUpTo | uint256[] | Latest withdrawal request that would be fulfilled if all current ETH will be spent to finalize requests. |
finalizationShareRates | uint256[] | Shares. Fetch from Lido smart contract |
This is the only field that could be tweaked using different strategies.
Reserve strategy V1
Total reserve amout is the amount of ether that would be enough to satisfy all withdrawal requests on the next report.
Formula is:
wr_amount - Sum all unfulfilled withdrawal requests.
wr_amount_predicted - All wr that could appear in buffer for next day.
eth_available - Sum EL and WC vaults.
eth_predicted - Sum all predicted income into EL and WC vaults.
withdrawable_validators - Sum of all Lido Validators with balance != 0
and withdrawable_epoch < current_report_epoch + epoch_in_frame
Exited validators for each Lido Node Operator will be reported in the next way:
Each column is a detail about each Node Operator. Node Operators without any changes in exited validators should be skipped to save gas fee.
Node operator #1 from module #1 has 10 exited validators.
Node operator #1 from module #2 has 10 exited validators.
Node operator #2 from module #2 has 20 exited validators.
Schema is:
Finalize all requests from last finalized to request #5 with share rate 100. And requests from #6 to #10 with share rate 110.
There are 3 vaults that would be used to fulfill withdrawal requests.
We should finalize as many requests as possible with current amount of ETH.
If bunker mode is on, withdrawal requests should not be finalized, so the report should contain empty lists.
Calculate first non-reported report.
Check el and wc balance.
To build next report use data from prev-report.
Use delta of el and wc balance.
(Make sure soft limits works)
Call view or static function to calculate shares after report and use this number. (handleOracleReport) Use result in finalizationShareRates. List contains only one element. (for testnet at least)
WithdrawalBus smart contract source code.
The ejector module decides which and how many validators should exit so that by the time we withdraw their balances, we can finalize all the current requests on this moment.
Field name | Type | Description |
---|---|---|
stakingModules | address[] | Source of Node Operator |
nodeOperatorIds | uint256[] | Node Operator ID in contract |
validatorIds | uint256[] | Validator's ID on Consensus Layer |
validatorPubkeys | bytes[] | Validator's pub key |
Validator with pubkey key1 with id #10 from NO #1 from module #1 should exit.
Validator with pubkey key2 with id #24 from NO #1 from module #2 should exit.
Oracle fulfills withdrawal requests from 3 sources:
The goal of the ejection module is to eject the minimum amount of validators which will be enough to fulfill all current unfinalized withdrawable requests (when the balance of these validators will be withdrawn).
So the formula is:
Where available_eth is:
predicted_eth - is amount of ETH that we receive until validators will be withdrawable.
Predicted ETH goes from different sources:
The time of prediction is the amount of time that took the validator to exit. It depends on the exit queue and NO reaction (NO should send signed exit message). The prediction works the next way.
In bunker mode, the prediction time will be equal to the amount of time from now till bunker mode ends.
So the final formula is:
Where:
N - amount of blocks takes validator to pass from oracle exit event till validator withdrawal.
- amount of validator to exit
- current EL rewards.
- predicted for EL rewards for N block.
- current WC balance.
- predicted amount of skimmed rewards.
- Validators that will pass withdrawal epoch in N blocks.
- All validators we asked to exit. But didn't reacted yet. No info in CL layer
- Validators asked to exit more than 7 days ago, but still exit message wasn't published.
Details are here.
Time to exit:
time_to_exit = withdrawable_epoch - current_epoch + time_to_send_exit_msg_epoch
Where:
epoch.
epochs.
epochs.
- values for median are calculated in this way - current_balance - N_block_behind_balance + oracle_transfer
where oracle_transfer
is ETH that oracle transferred out in this interval.
1
(64.1)->(32) (+32 + 0.1) = 32.1
(32.1, 32) ->(32, 0) (+32+ 0.1) =
Beacon_old - beacon_new
- current_balance - N_block_behind_balance + oracle_ransfer - withdrawn_validators
where withdrawn_validators
are all validators withdrawn in this interval.
- Read all events in 7 days for ejection and sum balances of this Oracles.
Big article with discussion is here.
As I see, there are 4 strategies:
For now, I've implemented the first strategy. This strategy is simplest to implement and understand.