To build a price oracle on Materia, you must first understand the requirements for your use case. Once you understand the kind of price average you require, it is a matter of storing the cumulative price variable from the pair as often as necessary, and computing the average price using two or more observations of the cumulative price variables.
To understand your requirements, you should first research the answer to the following questions:
Note your answers for the following discussion.
In the case where data freshness is not important and recent prices are weighted equally with historical prices, it is enough to store the cumulative price once per period (e.g. once per 24 hours.)
Computing the average price over these data points gives you ‘fixed windows’, which can be updated after the lapse of each period.
In the case where data freshness is important, you can use a sliding window in which the cumulative price variable is measured more often than once per period.
There are at least two kinds of moving averages that you can compute using the Materia cumulative price variable.
Simple moving averages give equal weight to each price measurement.
Exponential moving averages give more weight to the most recent price measurements. We do not yet have an example written for this type of oracle.
You may wish to use exponential moving averages where recent prices are more important than historical prices, e.g. in case of liquidations. However, note that putting more weight on recent prices makes the oracle cheaper to manipulate than weighting all price measurements equally.
To compute the average price given two cumulative price observations, take the difference between
the cumulative price at the beginning and end of the period, and
divide by the elapsed time between them in seconds. This will produce a
fixed point unsigned Q112x112
number that represents the price of one asset relative to the other. This number is represented as a uint224
where
the upper 112 bits represent the integer amount, and the lower 112 bits represent the fractional amount.
Pairs contain both price0CumulativeLast
and price1CumulativeLast
, which are ratios of reserves
of token1
/token0
and token0
/token1
respectively. I.e. the price of token0
is expressed in terms of
token1
/token0
, while the price of token1
is expressed in terms of token0
/token1
.
If you wish to compute the average price between a historical price cumulative observation and the current cumulative
price, you should use the cumulative price values from the current block. If the cumulative price has not been updated
in the current block, e.g. because there has not been any liquidity event (mint
/burn
/swap
) on the pair in the current
block, you can compute the cumulative price counterfactually.
The MateriaPair
cumulative price variables are designed to eventually overflow,
i.e. price0CumulativeLast
and price1CumulativeLast
and blockTimestampLast
will overflow through 0.
This should not pose an issue to your oracle design, as the price average computation is concerned with differences
(i.e. subtraction) between two separate observations of a cumulative price variable.
Subtracting between two cumulative price values will result in a number that fits within the range of uint256
as long
as the observations are made for periods of max 2^32
seconds, or ~136 years.
blockTimestampLast
is stored only in a uint32
. For the same reason as described above, the pair can save a
storage slot, and many SSTORES over the life of the pair, by storing only block.timestamp % uint32(-1)
.
This is feasible because the pair is only concerned with the time that elapses between each liquidity event when updating
the cumulative prices, which is always expected to be less than 2^32
seconds.
When computing time elapsed within your own oracle, you can simply store the block.timestamp
of your observations
as uint256
, and avoid dealing with overflow math for computing the time elapsed between observations.
To integrate an oracle into your contracts, you must ensure the oracle’s observations of the cumulative price variable are kept up to date. As long as your oracle is up to date, you can depend on it to produce average prices. The process of keeping your oracle up to date is called ‘maintenance’.
In order to measure average prices over a period, the oracle must have a way of referencing the cumulative price at the start and end of a period. The recommended way of doing this is by storing these prices in the oracle contract, and calling the oracle frequently enough to store the latest cumulative price.
Reliable oracle maintenance is a difficult task, and can become a point of failure in times of congestion. Instead, consider building this functionality directly into the critical calls of your own smart contracts, or incentivize oracle maintenance calls by other parties.
It is possible to avoid regularly storing this cumulative price at the start of the period by utilizing storage proofs. However, this approach has limitations, especially in regard to gas cost and maximum length of the time period over which the average price can be measured.