Analysis of the RAI TWAP Oracle

BT
Reflexer
Published in
7 min readNov 10, 2021

Overview of Price Feeds in the RAI System

The RAI system currently uses two external price feeds:

  1. Chainlink ETH/USD feed
  2. Uniswap V2 RAI/ETH pair

The Chainlink ETH/USD feed is used by the system to price ETH collateral. This price determines collateralization ratios and the system relies on it to trigger liquidations.

The RAI/ETH price is used in combination with the Chainlink ETH/USD feed to calculate the RAI/USD market price. The market price is then used to determine the deviation (aka “error”) from the system’s redemption price. In turn, this error is used by the PI/D controller (Money God) to calculate a new redemption rate, thus increasing or decreasing the internal value of RAI.

See here for a refresher on how the PID controller influences the system.

System Diagram

The top portion of the full GEB System Diagram shows the components of the system that we will be discussing. The left cloud represents the Chainlink ETH/USD feed and the right cloud represents the Uniswap V2 RAI/ETH pool.

The Chainlink ETH/USD feed is consumed by the Chainlink Relayer and the Uniswap V2 Pool prices are consumed by the Uniswap Medianizer.

Chainlink Relayer: ETH/USD

Chainlink ETH/USD Feed

The Chainlink ETH/USD feed is currently an aggregation of 31 off-chain oracles. The frequency of aggregated price updates, aka answers, are controlled by two trigger parameters.

  1. Deviation threshold: the percentage the off-chain aggregated price must move before a new on-chain answer is set
  2. Staleness: the longest time (in seconds) an on-chain answer can exist before an update is forced

The current settings for the Chainlink ETH/USD feed are:

Deviation Threshold = 0.5%
Staleness: 86,400 seconds

Therefore if the aggregated price of the oracles moves more than 0.5% or if it’s been at least 86,400 seconds since the last update, Chainlink will push a new price update.

To learn more about Chainlink feeds and their architecture, you can read the Chainlink docs.

Chainlink Relayer

The ETH/USD feed is initially consumed by the Chainlink Relayer. The latest ETH/USD answer is set in the Chainlink Relayer and then queried by two other system components:

  1. The OSM, which enforces a one hour delay between ETH collateral price updates
  2. The Uniswap Medianizer, which calculates the RAI/USD price

This post will focus on the Uniswap Medianizer since we are discussing the calculation of the RAI/USD market price.

Uniswap Medianizer: Combining RAI/ETH and ETH/USD

Uniswap V2 TWAPs

One feature of Uniswap V2 pairs is the ability to construct a time-weighted average price (TWAP). Each Uniswap pair contract tracks a cumulative price that represents the “sum of the Uniswap price for every second in the entire history of the contract”

From the Uniswap V2 oracle docs:

The TWAP is constructed by reading the cumulative price from an ERC20 token pair at the beginning and at the end of the desired interval. The difference in this cumulative price can then be divided by the length of the interval to create a TWAP for that period.

A TWAP for a specific time window can be constructed by calling the pair contract at the beginning and end of a window. The Uniswap Medianizer performs this type of sampling.

Uniswap Medianizer: RAI/ETH TWAP

Despite its name, the Uniswap Medianizer does not produce a median, but rather a Time-Weighted Average Price(TWAP). It does this by sampling Uniswap V2 priceCumulative and storing its recent values.

The following are Uniswap Medianizer parameters:

Period size: how often Uniswap V2 priceCumulative can be sampled
Window size: size of the window used for the TWAP
Max window size: largest possible window size allowed. Hard limit on how old the oldest priceCumulative sample can be. This allows extra time if price isn’t sampled exactly every period size due to gas spikes, off-chain outages, etc.

Uniswap Medianizer: ETH/USD TWAP

In addition to tracking the RAI/ETH TWAP, the Uniswap Medianizer also creates an ETH/USD TWAP, using the price data from the ChainlinkRelayer.

For the ETH/USD TWAP, the medianizer uses the same parameters (window size, period size and max window size). However, there is no ETH/USD priceCumulative variable that updates every block as in the Uniswap pair. The Medianizer maintains this value from periodic ETH/USD samples during the window, not after every block like the Uniswap RAI/ETH pair.

Uniswap Medianizer: Calculating the Final TWAP Price

To calculate the RAI/USD TWAP, the medianizer simply multiplies the RAI/ETH TWAP by the ETH/USD TWAP.

Let μ be the mean price over the previous window, the TWAP. Then:

RAI/USD TWAP = RAI/ETH TWAP * ETH/USD TWAP

For a complete example of how the RAI/USD TWAP is calculated, see the Appendix below.

Let’s see how the RAI/USD TWAP performs in the production system.

Results in Production

Below we can see a chart comparing two price series:

  1. Prod TWAP(blue): RAI/USD TWAP values produced by the Uniswap Medianizer
  2. Spot RAI/USD(red): Calculated by multiplying per block Uniswap V2 RAI/ETH price by per block Chainlink ETH/USD feed

Observations

Notice there are periods where the TWAP (blue) doesn’t seem to represent the spot prices from the previous window. Specifically, the TWAP dips around the 23rd and 25th don’t seem to match market behavior.

We noticed this mismatch between the TWAP price and recent RAI/USD market prices was happening consistently. There was more volatility in the RAI/USD TWAP than expected, given the volatility of RAI and ETH. This is an issue as even errors in the TWAP of 1% are significant . With current controller parameters, a 1% deviation between the TWAP price and redemption price creates around a positive or negative 6% annual redemption rate.

As the TWAP directly influences the rates of the system, it should have minimal error.

What is happening?

What is causing the production RAI/USD TWAP to not be representative of current market behavior?

  1. Estimation of ETH/USD TWAP.

While we can construct the exact RAI/ETH TWAP of a window through priceCumulative, we are estimating the TWAP of ETH/USD for this window through sampling.

The current production TWAP only uses 4 ETH/USD samples over a window of 16 hours to calculate the ETH/USD TWAP. This will inevitably lead to error from the true ETH/USD TWAP.

2. Chainlink ETH/USD deviation threshold.

As the Chainlink ETH/USD feed is not an instantaneous spot price, it can have up to 0.5% error from the true spot price. This error is unavoidable and will inevitably lead to some deviation from the true ETH/USD TWAP.

3. Not considering covariance when combining RAI/ETH and ETH/USD

If we go back to the Medianizer calculation, we are calculating the RAI/USD TWAP as:

RAI/USD TWAP = RAI/ETH TWAP ∗ ETH/USD TWAP

We’ve attempted to construct a third pair TWAP by multiplying the TWAPs of two composite pairs.

However, this is a product of two means and will not necessarily be equal to the mean of two products (what we actually want, the mean of RAI/ETH*ETH/USD).

Let’s explore this more.

When Is Our Assumption True?

Our assumption:
(RAI/ETH∗ETH/USD) TWAP = RAI/ETH TWAP ∗ ETH/USD TWAP

We can use the definition of covariance to see under what conditions the above statement is true. Consider the covariance of two random variables, X and Y. These represent RAI/ETH and ETH/USD prices in our context.

cov(X,Y)=E[(X−E[X])(Y−E[Y])]
=E[XY−XE[Y]−E[X]Y+E[X]E[Y]]
=E[XY]−E[X]E[Y]−E[X]E[Y]+E[X]E[Y]
=E[XY]−E[X]E[Y]

On the final line, we see that cov(X,Y) is equal to the mean of the product of two random variables minus the product of the means of two random variables.

Thus,

E[XY]=E[X]E[Y]⟺cov(X,Y)=0

The product of two means equals the means of two products if and only if the covariance of the two variables equals 0.

So the final RAI/ETH TWAP ∗ ETH/USD TWAP will only be accurate if RAI/ETH and ETH/USD have a covariance of 0.

Since we cannot expect the two pairs to have zero covariance, there will be some error in the third, combined TWAP.

Can We Fix This?

Using the above formula, we could possibly construct the true RAI/USD TWAP by adding the covariance to the product of the two TWAPs.

E[XY]=E[X]E[Y]+cov(X,Y)

However, we do not have access to all data points within the window to calculate cov(X,Y).

We could possibly estimate the covariance, but this estimate will be limited by how often we update the Uniswap Medianizer. The medianizer is updated every window_size/period_size hours. With current production settings of window_size=16, period_size=4, the covariance of a 16-hour window would be estimated from only 4 samples and most likely a poor estimate.

Further, this would need to be a time-weighted covariance to match the time-weighted prices used.

Extra Limitations

In addition to the shortcomings addressed, a TWAP constructed from Uniswap V2 RAI/ETH doesn’t cover all relevant RAI markets (e.g Coinbase, Uniswap V3 RAI/DAI, RAI/USDC, etc).

In the next post, we will explore a Chainlink RAI/USD price feed as the oracle for the RAI market price.

References

Uniswap Medianizer source code

Chainlink Relayer source code

GEB Oracle docs

Appendix

Example: RAI/ETH TWAP Calculation

Consider the following samples of RAI/ETH priceCumulative and these TWAP parameters:

window_size=600
period_size=120
granularity=600/120=5

Let:
pc_n=priceCumulative at block n
t_n=timestamp at block n

The current TWAP is calculated as such:

RAIETH_TWAPcurrent =(pc_502−pc_100)/ (t_502−t_100)

RAIETH_TWAPcurrent =(0.452619−0.070707)/(612−111)=0.381912/501=0.0007622994011976

Example: ETH/USD TWAP Calculation

Consider the following samples of the ETH/USD price and the same TWAP parameters.

Unlike RAI/ETH, there is no per-second cumulative price for the ETH/USD price. To simulate this, each ETH/USD price is manually weighted by time elapsed, then divided by the total time elapsed since the first sample.

Let:
p_n=price at block n
t_n=time at block n

Then:

ETHUSD_TWAPcurrent=(p_201∗(t_201−t_100)+p_300∗(t_300−t_201)+p_404∗(t_404−t_300)+p_502∗(t_502−t_404))/(t_502−t_100)

ETHUSD_TWAPcurrent=(4100∗(236−111)+4075∗(381−236)+4121∗(497−381)+4108∗(612−497))/(612−111)

ETHUSD_TWAPcurrent=4099.36

Example: Final RAI/USD Calculation

Finally, the two values are multiplied to yield the current RAI/USD TWAP value.

RAIUSD_TWAPcurrent=RAIETH_TWAPcurrent∗ETHUSD_TWAPcurrent

RAIUSD_TWAPcurrent=0.0007622994011976∗4099.36≈3.12

--

--