Skip to main content


LidoOracle is a contract where oracles send addresses' balances controlled by the DAO on the ETH 2.0 side. The balances can go up because of reward accumulation and can go down due to slashing and staking penalties. Oracles are assigned by the DAO.

Oracle daemons push their reports every frame (225 epochs currently, equal to one day) and when the number of the same reports reaches the 'quorum' value, the report is pushed to the Lido contract.


However, 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 on Beacon Chain. This would ultimately result in no oracle reports and no stETH rebases for this whole period.

The following mechanisms are also worth mentioning.

Store the collected reports as an array

The report variant is a report with a counter - how many times this report was pushed by oracles. This strongly simplified logic of _getQuorumReport, because in the majority of cases, we only have 1 variant of the report so we just make sure that its counter exceeded the quorum value.


The important note here is that when we remove an oracle (with removeOracleMember), we also need to remove her report from the currently accepted reports. As of now, we do not keep a mapping between members and their reports, we just clean all existing reports and wait for the remaining oracles to push the same epoch again.

Add calculation of staker rewards APR

To calculate the percentage of rewards for stakers, we store and provide the following data:

  • preTotalPooledEther - total pooled ether mount, queried right before every report push to the Lido contract,
  • postTotalPooledEther - the same, but queried right after the push,
  • lastCompletedEpochId - the last epoch that we pushed the report to the Lido,
  • timeElapsed - the time in seconds between the current epoch of push and the lastCompletedEpochId. Usually, it should be a frame long: 32 12 225 = 86400, but maybe multiples more in case that the previous frame didn't reach the quorum,
  • lidoFee - Lido's fee in basis points. Might be retrieved by calling getFee() in the Lido contract,
  • basisPoint - a constant that determines the accuracy of the fee, it's equal to 10000.

It is important to note here, that we collect post/pre pair (not current/last), to avoid the influence of new staking during the epoch.

To calculate the APR, use the following formula:

protocolAPR = (postTotalPooledEther - preTotalPooledEther) * secondsInYear / (preTotalPooledEther * timeElapsed)
lidoFeeAsFraction = lidoFee / basisPoint
userAPR = protocolAPR * (1 - lidoFeeAsFraction)

Sanity checks the oracles reports by configurable values

In order to limit the misbehaving oracles impact, we want to limit oracles report change by 10% APR increase in stake and 5% decrease in stake. Both values are configurable by the governance in case of extremely unusual circumstances.


Note that the change is evaluated after the quorum of oracles reports is reached, and not on the individual report.

And the logic of reporting to the Lido contract got a call to _reportSanityChecks that does the following. It compares the preTotalPooledEther and postTotalPooledEther (see above) and

  • if there is a profit or same, calculates the APR, compares it with the upper bound. If was above, reverts the transaction with ALLOWED_BEACON_BALANCE_INCREASE code.
  • if there is a loss, calculates relative decrease and compares it with the lower bound. If was below, reverts the transaction with ALLOWED_BEACON_BALANCE_DECREASE code.

Receiver function to be invoked on report pushes

To provide the external contract with updates on report pushes (every time the quorum is reached among oracle daemons data), we provide the following setter and getter functions. It might be needed to implement some updates to the external contracts that should happen at the same tx the rebase happens (e.g. adjusting uniswap v2 pools to reflect the rebase).

And when the callback is set, the following function will be invoked on every report push.

interface IBeaconReportReceiver {
function processLidoOracleReport(uint256 _postTotalPooledEther,
uint256 _preTotalPooledEther,
uint256 _timeElapsed) external;

The arguments provided are the same as described in section above.

See also the CompositePostRebaseBeaconReceiver adapter contract which allows to set multiple callbacks.

View Methods


Return the Lido contract address.

function getLido() returns (ILido)


Return the number of exactly the same reports needed to finalize the epoch.

function getQuorum() returns (uint256)


Return the upper bound of the reported balance possible increase in APR. See above about sanity checks.

function getAllowedBeaconBalanceAnnualRelativeIncrease() returns (uint256)


Return the lower bound of the reported balance possible decrease. See above about sanity checks.

function getAllowedBeaconBalanceRelativeDecrease() returns (uint256)


Return the receiver contract address to be called when the report is pushed to Lido.

function getBeaconReportReceiver() returns (address)


Return the current reporting bitmap, representing oracles who have already pushed their version of report during the expected epoch.


Every oracle bit corresponds to the index of the oracle in the current members list

function getCurrentOraclesReportStatus() returns (uint256)


Return the current reporting variants array size.

function getCurrentReportVariantsSize() returns (uint256)


Return the current reporting array element with index _index.

function getCurrentReportVariant(uint256 _index)


Return epoch that can be reported by oracles.

function getExpectedEpochId() returns (uint256)


Return the current oracle member committee list.

function getOracleMembers() returns (address[])


Return the initialized version of this contract starting from 0.

function getVersion() returns (uint256)


Return beacon specification data.

function getBeaconSpec()
returns (
uint64 epochsPerFrame,
uint64 slotsPerEpoch,
uint64 secondsPerSlot,
uint64 genesisTime


Return the epoch calculated from current timestamp.

function getCurrentEpochId() returns (uint256)


Return currently reportable epoch (the first epoch of the current frame) as well as its start and end times in seconds.

function getCurrentFrame()
returns (
uint256 frameEpochId,
uint256 frameStartTime,
uint256 frameEndTime


Return last completed epoch.

function getLastCompletedEpochId() returns (uint256)


Report beacon balance and its change during the last frame.

function getLastCompletedReportDelta()
returns (
uint256 postTotalPooledEther,
uint256 preTotalPooledEther,
uint256 timeElapsed



Set the upper bound of the reported balance possible increase in APR to _value. See above about sanity checks.

function setAllowedBeaconBalanceAnnualRelativeIncrease(uint256 _value) auth(SET_REPORT_BOUNDARIES)


Set the lower bound of the reported balance possible decrease to _value. See above about sanity checks.

function setAllowedBeaconBalanceRelativeDecrease(uint256 _value) auth(SET_REPORT_BOUNDARIES)


Set the receiver contract address to _addr to be called when the report is pushed.


Specify 0 to disable this functionality. The receiver contract MUST implement EIP-165.

function setBeaconReportReceiver(address _addr) auth(SET_BEACON_REPORT_RECEIVER)


Update beacon specification data.

function setBeaconSpec(
uint64 _epochsPerFrame,
uint64 _slotsPerEpoch,
uint64 _secondsPerSlot,
uint64 _genesisTime


Initialize the contract to perform v0 → v3 transition.


The function initialize could not be called twice and it needed once the contract is initialized for the first time (i.e. deploying from scratch). For more details see the Lido improvement proposal #10.

function initialize(
address _lido,
uint64 _epochsPerFrame,
uint64 _slotsPerEpoch,
uint64 _secondsPerSlot,
uint64 _genesisTime,
uint256 _allowedBeaconBalanceAnnualRelativeIncrease,
uint256 _allowedBeaconBalanceRelativeDecrease
) external


A function to finalize upgrade to v3 (from v1). Can be called only once.


v2 is skipped due to a change in numbering. For more details see the Lido improvement proposal #10.

function finalizeUpgrade_v3() external


Add _member to the oracle member committee list.

function addOracleMember(address _member) auth(MANAGE_MEMBERS)


Remove '_member` from the oracle member committee list.

function removeOracleMember(address _member) auth(MANAGE_MEMBERS)


Set the number of exactly the same reports needed to finalize the epoch to _quorum.

function setQuorum(uint256 _quorum) auth(MANAGE_QUORUM)


Accept oracle committee member reports from the Ethereum side. Parameters:

  • _epochId - beacon chain epoch
  • _beaconBalance - balance in gwei on the Ethereum side (9-digit denomination)
  • _beaconValidators - number of validators visible in this epoch
function reportBeacon(uint256 _epochId, uint64 _beaconBalance, uint32 _beaconValidators)