Skip to main content

LegacyOracle

warning

LegacyOracle will be maintained till the end of 2023. Afterwards, it will be discontinued and external integrations should rely on AccountingOracle.

What is LegacyOracle?

LegacyOracle is an Aragon app previously known as LidoOracle, used to track changes on the Beacon Chain. Following the Lido V2 upgrade, this was replaced by the AccountingOracle and the oracle workflow was redesigned to deliver synchronized historical data chunks for the same reference slot both for the Consensus and Execution Layer parts.

Key changes

In Lido V2, LegacyOracle only supports a subset of view functions and events. AccountingOracle interacts with it to push data changes on each report.

How does LegacyOracle receive the AccountingOracle reports anyway (flow)

The LegacyOracle contract receives the data changes on each AccountingOracle report using two stages (still within the same transaction):

  1. Invoke handleConsensusLayerReport providing the reference slot and validators data from AccountingOracle itself.
  2. Invoke handlePostTokenRebase from Lido.

Rebase and APR

To calculate the protocol's daily rebase and APR projections one would use the old LidoOracle APIs for a while. Although the old way of calculating the APR would still result in relevant numbers, the math might be off in case of significant withdrawals.

How it was with LidoOracle

note

The formula is outdated and inaccurate since the Lido V2 upgrade happened.

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

What's new from Lido V2

See the new Lido API docs with regards to APR.

// 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

In short, the new formula takes into account both preTotalShares and postTotalShares values, while, in contrast, the old formula didn't use them. The new formula also doesn't require to calculate lidoFee at all (because the fee distribution works by changing the total shares amount under the hood).

Why does it matter

When Lido V2 protocol finalizes withdrawal requests, the Lido contract sends ether to WithdrawalQueue (excluding these funds from totalPooledEther, i.e., decreasing TVL) and assigns to burn underlying locked requests' stETH shares in return.

In other words, withdrawal finalization decreases both TVL and total shares.

Old formula isn't suitable anymore because it catches TVL changes, but skips total shares changes.

Illustrative example (using smallish numbers far from the real ones for simplicity):

preTotalEther = 1000 ETH
preTotalShares = 1000 * 10^18 // 1 share : 1 wei

postTotalEther = 999 ETH
postTotalShares = 990 * 10^18

timeElapsed = 24 * 60 * 60 // 1 day, or 86400 seconds

//!!! using the old (imprecise) method

// protocolAPR = (postTotalPooledEther - preTotalPooledEther) * secondsInYear / (preTotalPooledEther * timeElapsed)
protocolAPR = (999ETH - 1000ETH) * 31557600 / (1000ETH * 86400) = -0.36525
//lidoFeeAsFraction = lidoFee / basisPoint = 0.1
//userAPR = protocolAPR * (1 - lidoFeeAsFraction) = protocolAPR * (1 - 0.1)

userAPR = -0.36525 * (1 - 0.1) = -0.328725

//!!! i.e, userAPR now is ~minus 32.9%

//!!! using the updated (proper) method

preShareRate = 1000 ETH * 1e27 / 1000 * 10^18 = 1e27
postShareRate = 999 ETH * 1e27 / 990 * 10^18 = 1.009090909090909e+27
userAPR = 31557600 * ((postShareRate - preShareRate) / preShareRate) / 86400 = 3.320454545454529

//!!! i.e., userAPR now is ~plus 332%

View Methods

getLido()

Returns the Lido contract address.

function getLido() returns (address)
note

Always returns the Lido address stated in the deployed addresses list.

getAccountingOracle()

Returns the AccountingOracle contract address.

function getAccountingOracle() returns (address)
note

Always returns the AccountingOracle address stated in the deployed addresses list.

getContractVersion()

Returns the current contract version.

function getContractVersion() returns (uint256)
note

Always returns 4.

getVersion()

Returns the current contract version (compatibility method).

function getVersion() returns (uint256)
note

Always returns 4, calls getContractVersion() internally.

getBeaconSpec()

Returns the AccountingOracle frame period together with Ethereum Beacon Chain specification constants.

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

Always returns (225, 32, 12, 1606824023) for Mainnet and (225, 32, 12, 1616508000) for Görli.

Returns

NameTypeDescription
epochsPerFrameuint64Beacon Chain epochs per single AccountingOracle report frame
slotsPerEpochuint64Beacon Chain slots per single Beacon Chain epoch
secondsPerSlotuint64Seconds per single Beacon Chain slot
genesisTimeuint64Beacon Chain genesis timestamp

getCurrentEpochId()

Returns the Beacon Chain epoch id calculated from the current timestamp using the beacon chain spec.

function getCurrentEpochId() returns (uint256)

getCurrentFrame()

Returns the first epoch of the current AccountingOracle reporting frame as well as its start and end times in seconds.

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

Returns

NameTypeDescription
frameEpochIduint256The first epoch of the current AccountingOracle reporting frame
frameStartTimeuint256The start timestamp of the current reporting frame
frameEndTimeuint256The end timestamp of the current reporting frame

getLastCompletedEpochId()

Returns the starting epoch of the last frame in which the last AccountingOracle report was received and applied.

function getLastCompletedEpochId() returns (uint256)

getLastCompletedReportDelta()

Returns the total supply change ocurred with the last completed AccountingOracle report.

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

Returns

NameTypeDescription
postTotalPooledEtheruint256Post-report `stETH`` total pooled ether (i.e., total supply)
preTotalPooledEtheruint256Pre-report stETH total pooled ether (i.e., total supply)
timeElapseduint256Time elapsed since the previously completed report, seconds

Methods

handlePostTokenRebase()

Handles a stETH token rebase incurred by the succeeded AccountingOracle report storing the total ether and time elapsed stats.

Emits PostTotalShares

function handlePostTokenRebase(
uint256 reportTimestamp,
uint256 timeElapsed,
uint256 preTotalShares,
uint256 preTotalEther,
uint256 postTotalShares,
uint256 postTotalEther,
uint256 totalSharesMintedAsFees
)
note

The caller must be Lido.

Parameters

NameTypeDescription
reportTimestampuint256The reference timestamp corresponding to the moment of the oracle report calculation
timeElapseduint256Time elapsed since the previously completed report, seconds
preTotalSharesuint256Pre-report stETH total shares
preTotalEtheruint256Pre-report stETH total pooled ether (i.e., total supply)
postTotalSharesuint256Post-report stETH total shares
postTotalEtheruint256Post-report stETH total pooled ether (i.e., total supply)
totalSharesMintedAsFeesuint256Total shares amount minted as the protocol fees on top of the accrued rewards

handleConsensusLayerReport()

Handles a new completed AccountingOracle report storing the corresponding Beacon Chain epoch id.

Emits Completed.

function handleConsensusLayerReport(
uint256 _refSlot,
uint256 _clBalance,
uint256 _clValidators
)
note

The caller must be AccountingOracle.

Parameters

NameTypeDescription
_refSlotuint256The reference slot corresponding to the moment of the oracle report calculation
_clBalanceuint256Lido-participating validators balance on the Beacon Chain side
_clValidatorsuint256Number of the Lido-participating validators on the Beacon Chain side

Events

Completed()

Emits whenever the AccountingOracle report landed.

This event is still emitted after oracle committee reaches consensus on a report, but only for compatibility purposes. The values in this event are not enough to calculate APR or TVL anymore due to withdrawals, Execution Layer rewards, and Consensus Layer rewards skimming.

event Completed(
uint256 epochId,
uint128 beaconBalance,
uint128 beaconValidators
);
note

Emits inside the handleConsensusLayerReport methods.

Parameters

NameTypeDescription
epochIduint256Report reference epoch identifier
beaconBalanceuint128The balance of the Lido-participating validators on the Consensus Layer side
beaconValidatorsuint128The number of the ever appeared Lido-participating validators

PostTotalShares()

Emits whenever the AccountingOracle report landed.

This event is still emitted after each rebase but only for compatibility purposes. The values in this event are not enough to correctly calculate the rebase APR since a rebase can result from shares burning without changing total ETH held by the protocol.

event PostTotalShares(
uint256 postTotalPooledEther,
uint256 preTotalPooledEther,
uint256 timeElapsed,
uint256 totalShares
)
note

The new TokenRebased event emitted from the main Lido contract should be used instead because it provides the pre-report total shares amount as well which is essential to properly estimate a token rebase and its projected APR.

Parameters

NameTypeDescription
postTotalPooledEtheruint256Post-report total pooled ether
preTotalPooledEtheruint256Pre-report total pooled ether
timeElapseduint256Time elapsed since the previous report, seconds
totalSharesuint256Post-report total shares