To build a price oracle on Pegasys, 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:
- Is data freshness important? I.e.: must the price average include the current price?
- Are recent prices more important than historical prices? I.e.: is the current price given more weight than historical prices?
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 Pegasys 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
the upper 112 bits represent the integer amount, and the lower 112 bits represent the fractional amount.
Pairs contain both
price1CumulativeLast, which are ratios of reserves
token1 respectively. I.e. the price of
token0 is expressed in terms of
token0, while the price of
token1 is expressed in terms of
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 (
swap) on the pair in the current
block, you can compute the cumulative price counterfactually.
We provide a library for use in oracle contracts that has the method
for getting the cumulative price as of the current block.
The current cumulative price returned by this method is computed counterfactually, meaning it requires no call to
the relative gas-expensive
#sync method on the pair.
It is correct regardless of whether a swap has already executed in the current block.
PegasysPair cumulative price variables are designed to eventually overflow,
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
When computing time elapsed within your own oracle, you can simply store the
block.timestamp of your observations
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. If you wish to try this approach, you can follow this repository by Keydonix.
Keydonix has developed a general purpose price feed oracle built on Uniswap v2 that supports arbitrary time windows (up to 256 blocks) and doesn't require any active maintenance.