Skip to content

Commit 848be2a

Browse files
authored
Merge pull request #234 from lidofinance/doc/legacy-oracle
Doc: LegacyOracle rework
2 parents 3fe7a36 + abf4a43 commit 848be2a

File tree

1 file changed

+366
-3
lines changed

1 file changed

+366
-3
lines changed

docs/contracts/legacy-oracle.md

Lines changed: 366 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,371 @@
44
- [Deployed contract](https://etherscan.io/address/0x442af784A788A5bd6F42A01Ebe9F287a871243fb)
55

66
:::warning
7-
LegacyOracle is used to be a previous oracle contract for Lido.
8-
It's left currently for compatibility reasons only and might be deprecated completely in the future releases.
7+
`LegacyOracle` will be maintained till the end of 2023.
8+
Afterwards, it will be discontinued and external integrations should rely on [`AccountingOracle`](/contracts/accounting-oracle).
99
:::
1010

11-
Superseded with [AccountingOracle](/contracts/accounting-oracle).
11+
## What is LegacyOracle?
12+
13+
`LegacyOracle` is an Aragon app previously known as `LidoOracle`, used to track changes on the Beacon Chain.
14+
Following the Lido V2 upgrade, this was replaced by the [`AccountingOracle`](/contracts/accounting-oracle)
15+
and the oracle workflow was redesigned to deliver synchronized historical data chunks for the same reference slot
16+
both for the Consensus and Execution Layer parts.
17+
18+
## Key changes
19+
20+
In Lido V2, `LegacyOracle` only supports a subset of view functions and events.
21+
`AccountingOracle` interacts with it to push data changes on each report.
22+
23+
### How does LegacyOracle receive the AccountingOracle reports anyway (flow)
24+
25+
The `LegacyOracle` contract receives the data changes on each `AccountingOracle` report using two stages
26+
(still within the same transaction):
27+
28+
1. Invoke [`handleConsensusLayerReport`](/contracts/legacy-oracle#handleConsensusLayerReport)
29+
providing the reference slot and validators data from `AccountingOracle` itself.
30+
1. Invoke [`handlePostTokenRebase`](/contracts/legacy-oracle#handlePostTokenRebase)
31+
from [`Lido`](/contracts/lido).
32+
33+
```mermaid
34+
graph LR;
35+
A[/ \]--submitReportData-->AccountingOracle--handleConsensusLayerReport--->LegacyOracle;
36+
AccountingOracle--handleOracleReport-->Lido--handlePostTokenRebase-->LegacyOracle
37+
```
38+
39+
### Rebase and APR
40+
41+
To calculate the protocol's daily rebase and APR projections one would use the old `LidoOracle` APIs for a while.
42+
Although the old way of calculating the APR would still result in relevant numbers, the math might be off in case of significant withdrawals.
43+
44+
#### How it was with LidoOracle
45+
46+
:::note
47+
The formula is outdated and inaccurate since the [Lido V2 upgrade](https://blog.lido.fi/lido-v2-launch/) happened.
48+
:::
49+
50+
```javascript
51+
protocolAPR = (postTotalPooledEther - preTotalPooledEther) * secondsInYear / (preTotalPooledEther * timeElapsed)
52+
lidoFeeAsFraction = lidoFee / basisPoint
53+
userAPR = protocolAPR * (1 - lidoFeeAsFraction)
54+
```
55+
56+
#### What's new from Lido V2
57+
58+
See the new Lido API docs with regards to [APR](/integrations/api#LidoAPR).
59+
60+
```js
61+
// Emits when token rebased (total supply and/or total shares were changed)
62+
event TokenRebased(
63+
uint256 indexed reportTimestamp,
64+
uint256 timeElapsed,
65+
uint256 preTotalShares,
66+
uint256 preTotalEther, /* preTotalPooledEther */
67+
uint256 postTotalShares,
68+
uint256 postTotalEther, /* postTotalPooledEther */
69+
uint256 sharesMintedAsFees /* fee part included in `postTotalShares` */
70+
);
71+
72+
preShareRate = preTotalEther * 1e27 / preTotalShares
73+
postShareRate = postTotalEther * 1e27 / postTotalShares
74+
75+
userAPR =
76+
secondsInYear * (
77+
(postShareRate - preShareRate) / preShareRate
78+
) / timeElapsed
79+
```
80+
81+
In short, the new formula takes into account both `preTotalShares` and `postTotalShares` values, while,
82+
in contrast, the old formula didn't use them. The new formula also doesn't require to calculate `lidoFee`
83+
at all (because the fee distribution works by changing the total shares amount under the hood).
84+
85+
#### Why does it matter
86+
87+
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.
88+
89+
In other words, withdrawal finalization decreases both TVL and total shares.
90+
91+
Old formula isn't suitable anymore because it catches TVL changes, but skips total shares changes.
92+
93+
Illustrative example (using smallish numbers far from the real ones for simplicity):
94+
95+
```javascript
96+
preTotalEther = 1000 ETH
97+
preTotalShares = 1000 * 10^18 // 1 share : 1 wei
98+
99+
postTotalEther = 999 ETH
100+
postTotalShares = 990 * 10^18
101+
102+
timeElapsed = 24 * 60 * 60 // 1 day, or 86400 seconds
103+
104+
//!!! using the old (imprecise) method
105+
106+
// protocolAPR = (postTotalPooledEther - preTotalPooledEther) * secondsInYear / (preTotalPooledEther * timeElapsed)
107+
protocolAPR = (999ETH - 1000ETH) * 31557600 / (1000ETH * 86400) = -0.36525
108+
//lidoFeeAsFraction = lidoFee / basisPoint = 0.1
109+
//userAPR = protocolAPR * (1 - lidoFeeAsFraction) = protocolAPR * (1 - 0.1)
110+
111+
userAPR = -0.36525 * (1 - 0.1) = -0.328725
112+
113+
//!!! i.e, userAPR now is ~minus 32.9%
114+
115+
//!!! using the updated (proper) method
116+
117+
preShareRate = 1000 ETH * 1e27 / 1000 * 10^18 = 1e27
118+
postShareRate = 999 ETH * 1e27 / 990 * 10^18 = 1.009090909090909e+27
119+
userAPR = 31557600 * ((postShareRate - preShareRate) / preShareRate) / 86400 = 3.320454545454529
120+
121+
//!!! i.e., userAPR now is ~plus 332%
122+
```
123+
124+
## View Methods
125+
126+
### getLido()
127+
128+
Returns the `Lido` contract address.
129+
130+
```sol
131+
function getLido() returns (address)
132+
```
133+
134+
:::note
135+
Always returns the `Lido` address stated in the [deployed addresses](/deployed-contracts) list.
136+
:::
137+
138+
### getAccountingOracle()
139+
140+
Returns the `AccountingOracle` contract address.
141+
142+
```sol
143+
function getAccountingOracle() returns (address)
144+
```
145+
146+
:::note
147+
Always returns the `AccountingOracle` address stated in the [deployed addresses](/deployed-contracts) list.
148+
:::
149+
150+
### getContractVersion()
151+
152+
Returns the current contract version.
153+
154+
```sol
155+
function getContractVersion() returns (uint256)
156+
```
157+
158+
:::note
159+
Always returns `4`.
160+
:::
161+
162+
### getVersion()
163+
164+
Returns the current contract version (compatibility method).
165+
166+
```sol
167+
function getVersion() returns (uint256)
168+
```
169+
170+
:::note
171+
Always returns `4`, calls `getContractVersion()` internally.
172+
:::
173+
174+
### getBeaconSpec()
175+
176+
Returns the `AccountingOracle` frame period together with Ethereum Beacon Chain specification constants.
177+
178+
```sol
179+
function getBeaconSpec() returns (
180+
uint64 epochsPerFrame,
181+
uint64 slotsPerEpoch,
182+
uint64 secondsPerSlot,
183+
uint64 genesisTime
184+
)
185+
```
186+
187+
:::note
188+
Always returns (225, 32, 12, 1606824023) for Mainnet and (225, 32, 12, 1616508000) for Görli.
189+
:::
190+
191+
#### Returns
192+
193+
| Name | Type | Description |
194+
| ---------------- | -------- | -------------------------------------------------------------- |
195+
| `epochsPerFrame` | `uint64` | Beacon Chain epochs per single `AccountingOracle` report frame |
196+
| `slotsPerEpoch` | `uint64` | Beacon Chain slots per single Beacon Chain epoch |
197+
| `secondsPerSlot` | `uint64` | Seconds per single Beacon Chain slot |
198+
| `genesisTime` | `uint64` | Beacon Chain genesis timestamp |
199+
200+
### getCurrentEpochId()
201+
202+
Returns the Beacon Chain epoch id calculated from the current timestamp using the [beacon chain spec](/contracts/legacy-oracle#getBeaconSpec).
203+
204+
```sol
205+
function getCurrentEpochId() returns (uint256)
206+
```
207+
208+
### getCurrentFrame()
209+
210+
Returns the first epoch of the current `AccountingOracle` reporting frame as well as its start and end times in seconds.
211+
212+
```sol
213+
function getCurrentFrame() returns (
214+
uint256 frameEpochId,
215+
uint256 frameStartTime,
216+
uint256 frameEndTime
217+
)
218+
```
219+
220+
#### Returns
221+
222+
| Name | Type | Description |
223+
| ----------------- | ---------- | ----------------------------------------------------------------- |
224+
| `frameEpochId` | `uint256` | The first epoch of the current `AccountingOracle` reporting frame |
225+
| `frameStartTime` | `uint256` | The start timestamp of the current reporting frame |
226+
| `frameEndTime` | `uint256` | The end timestamp of the current reporting frame |
227+
228+
### getLastCompletedEpochId()
229+
230+
Returns the starting epoch of the last frame in which the last `AccountingOracle` report was received and applied.
231+
232+
```sol
233+
function getLastCompletedEpochId() returns (uint256)
234+
```
235+
236+
### getLastCompletedReportDelta()
237+
238+
Returns the total supply change ocurred with the last completed `AccountingOracle` report.
239+
240+
```sol
241+
function getLastCompletedReportDelta() returns (
242+
uint256 postTotalPooledEther,
243+
uint256 preTotalPooledEther,
244+
uint256 timeElapsed
245+
)
246+
```
247+
248+
#### Returns
249+
250+
| Name | Type | Description |
251+
| ------------------------- | ---------- | ------------------------------------------------------------- |
252+
| `postTotalPooledEther` | `uint256` | Post-report `stETH`` total pooled ether (i.e., total supply) |
253+
| `preTotalPooledEther` | `uint256` | Pre-report `stETH` total pooled ether (i.e., total supply) |
254+
| `timeElapsed` | `uint256` | Time elapsed since the previously completed report, seconds |
255+
256+
## Methods
257+
258+
### handlePostTokenRebase()
259+
260+
Handles a `stETH` token rebase incurred by the succeeded `AccountingOracle` report storing
261+
the total ether and time elapsed stats.
262+
263+
Emits [`PostTotalShares`](/contracts/legacy-oracle#PostTotalShares)
264+
265+
```sol
266+
function handlePostTokenRebase(
267+
uint256 reportTimestamp,
268+
uint256 timeElapsed,
269+
uint256 preTotalShares,
270+
uint256 preTotalEther,
271+
uint256 postTotalShares,
272+
uint256 postTotalEther,
273+
uint256 totalSharesMintedAsFees
274+
)
275+
```
276+
277+
:::note
278+
The caller must be `Lido`.
279+
:::
280+
281+
#### Parameters
282+
283+
| Name | Type | Description |
284+
| ------------------------- | ---------- | ------------------------------------------------------------------------------------- |
285+
| `reportTimestamp` | `uint256` | The reference timestamp corresponding to the moment of the oracle report calculation |
286+
| `timeElapsed` | `uint256` | Time elapsed since the previously completed report, seconds |
287+
| `preTotalShares` | `uint256` | Pre-report `stETH` total shares |
288+
| `preTotalEther` | `uint256` | Pre-report `stETH` total pooled ether (i.e., total supply) |
289+
| `postTotalShares` | `uint256` | Post-report `stETH` total shares |
290+
| `postTotalEther` | `uint256` | Post-report `stETH` total pooled ether (i.e., total supply) |
291+
| `totalSharesMintedAsFees` | `uint256` | Total shares amount minted as the protocol fees on top of the accrued rewards |
292+
293+
### handleConsensusLayerReport()
294+
295+
Handles a new completed `AccountingOracle` report storing the corresponding Beacon Chain epoch id.
296+
297+
Emits [`Completed`](/contracts/legacy-oracle#Completed).
298+
299+
```sol
300+
function handleConsensusLayerReport(
301+
uint256 _refSlot,
302+
uint256 _clBalance,
303+
uint256 _clValidators
304+
)
305+
```
306+
307+
:::note
308+
The caller must be `AccountingOracle`.
309+
:::
310+
311+
#### Parameters
312+
313+
| Name | Type | Description |
314+
| ---------------- | ---------- | -------------------------------------------------------------------------------- |
315+
| `_refSlot` | `uint256` | The reference slot corresponding to the moment of the oracle report calculation |
316+
| `_clBalance` | `uint256` | Lido-participating validators balance on the Beacon Chain side |
317+
| `_clValidators` | `uint256` | Number of the Lido-participating validators on the Beacon Chain side |
318+
319+
## Events
320+
321+
### Completed()
322+
323+
Emits whenever the `AccountingOracle` report landed.
324+
325+
This event is still emitted after oracle committee reaches consensus on a report, but only for compatibility purposes.
326+
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.
327+
328+
```solidity
329+
event Completed(
330+
uint256 epochId,
331+
uint128 beaconBalance,
332+
uint128 beaconValidators
333+
);
334+
```
335+
336+
:::note
337+
Emits inside the [`handleConsensusLayerReport`](/contracts/legacy-oracle#handleConsensusLayerReport) methods.
338+
:::
339+
340+
#### Parameters
341+
342+
| Name | Type | Description |
343+
| ------------------ | --------- | ---------------------------------------------------------------------------- |
344+
| `epochId` | `uint256` | Report reference epoch identifier |
345+
| `beaconBalance` | `uint128` | The balance of the Lido-participating validators on the Consensus Layer side |
346+
| `beaconValidators` | `uint128` | The number of the ever appeared Lido-participating validators |
347+
348+
### PostTotalShares()
349+
350+
Emits whenever the `AccountingOracle` report landed.
351+
352+
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.
353+
354+
```solidity
355+
event PostTotalShares(
356+
uint256 postTotalPooledEther,
357+
uint256 preTotalPooledEther,
358+
uint256 timeElapsed,
359+
uint256 totalShares
360+
)
361+
```
362+
363+
:::note
364+
The new [`TokenRebased`](/contracts/lido#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.
365+
:::
366+
367+
#### Parameters
368+
369+
| Name | Type | Description |
370+
| ---------------------- | --------- | ----------------------------------------------- |
371+
| `postTotalPooledEther` | `uint256` | Post-report total pooled ether |
372+
| `preTotalPooledEther` | `uint256` | Pre-report total pooled ether |
373+
| `timeElapsed` | `uint256` | Time elapsed since the previous report, seconds |
374+
| `totalShares` | `uint256` | Post-report total shares |

0 commit comments

Comments
 (0)