A Guide on Uniswap v3 TWAP Oracle

Implementation, code tracing and an explanation for the minor imprecision.

Image source: https://uniswap.org/blog/uniswap-v3/

Outline

0. Intro
1. Uniswap v3 TWAP Implementation
- Note: Minor Imprecision
2. Getting TWAP
- Note: increaseObservationCardinalityNext()
3. Uniswap v3 TWAP Oracle vs Chainlink
4. Conclusion

0. Intro

TWAP stands for the time-weighted average price. It’s a reliable average price that can exclude short-term price fluctuation or manipulation and have been widely used in DeFi.

Recently, I stepped into the minor imprecision issue of the TWAP offered by Uniswap v3 for the second time, which reminded me of the necessity of putting this down.

The built-in TWAP oracle of Uniswap v3 is not only convenient but also extremely easy to use. Here’s a code snippet from charm.fi:

Source: https://github.com/charmfinance/alpha-vaults-contracts/blob/07db2b213315eea8182427be4ea51219003b8c1a/contracts/AlphaStrategy.sol#L136

Less than 10 lines of code… though this is not the full story of getting TWAP, but rather getting the time-weighted average tick, this extra feature of Uniswap v3 does save hours for many projects from building their own TWAP implementation!

Now, before we get started, the editor’s choice of music as usual!

1. Uniswap v3 TWAP Implementation

The time-weighted price algorithm is quite simple: the price P multiplied by how long it lasts T is continuously added to a cumulative value C.

For example,

  • when timestamp = 0 & ETH price = 3000: C = 0 (initialisation)
  • when timestamp = 200 & ETH price = 3200: C = 0 + 3000 * (200 — 0) = 600,000
  • when timestamp = 250 & ETH price = 3150: C = 600,000 + 3200 * (250 — 200) = 760,000

The TWAP between time (0, 250) is (760,000 — 0) / (250 — 0) = 3,040, which satisfies that price 3000 lasts for 4/5 of the time and price 3200 lasts for 1/5: 3000 * 4 / 5 + 3200 / 5 = 3,040.

Source: https://github.com/Uniswap/uniswap-v3-core/blob/3e88af408132fc957e3e406f65a0ce2b1ca06c3d/contracts/libraries/Oracle.sol#L30

- Note: Minor Imprecision

One thing worth noticing in the above code is that Uniswap v3 time-weights the tick instead of the price. (Recommend going through Uniswap v3 Features Explained in Depth if you have no idea what are tick and price.)

For example, a price of 3000 is equivalent to tick 80067.678794 on Uniswap v3, for1.0001 ^ 80067.678794 ~= 3000. However, the tick parameter in Uniswap v3 has always been an integer, which always discards numbers behind the decimal point. In this case, it becomes 80067.

This is the reason for the minor imprecision I mentioned I got into. Too many details to remember :(

2. Getting TWAP

After knowing the algorithm, let’s figure out how to get the TWAP.

As I mentioned earlier, the code from charm.fi only fetches the time-weighted tick, instead of the time-weighted price. Thus, based on charm’s code, I write up an extended version:

Getting TWAP from the time-weighted tick requires only one extra function TickMath.getSqrtRatioAtTick(), which maps tick to price.

Update on August 24
There is one issue here that I got myself confused about when reviewing the code: whether the casting of (tickCumulatives[1] — tickCumulatives[0]) / twapInterval to int24 can overflow or underflow.
However, there is no such as risk as tickCumulatives is fetched with the input twapInterval, and we divide (tickCumulatives[1] — tickCumulatives[0]) by twapInterval, which means the time weight of the tick should be the same as twapInterval.

Thanks to Kimi Wu for pointing this out. As the old saying goes, the spectators see the chess game better than the players …

Besides getting TWAP, the above code snippet also includes

  • edge case handling when twapInterval == 0; return the current price of the pool
  • a helper function to transform sqrtPrice to price, which might be helpful for certain use cases that need normal prices instead of prices in square root. The Fullmath.mulDiv() function can handle the overflow of the multiplication of the two sqrtPrice, which can be looked up here

Notice that prices on Uniswap v3 are always scaled up by 2⁹⁶, which is the reason why I add the suffix X96 to all price-related variables.

- Note: increaseObservationCardinalityNext()

When testing out the above code, there is one more thing to pay attention to: when a pool is initialized, there is only one slot for storing prices for TWAP calculation.

The more slots a pool has, TWAPs over longer periods can be provided.

Therefore, we can use the function increaseObservationCardinalityNext() in UniswapV3Pool.sol to expand the number of slots available.

3. Uniswap v3 TWAP Oracle vs Chainlink

So, with the existence of this TWAP oracle, is Chainlink useless? Absolutely not.

While Uniswap v3 offers the TWAP oracle, a project might want to calculate the Moving Average, the Exponential Moving Average… which means Chainlink and other oracle solutions are and have always been indispensable for creating these customised price indicators.

Furthermore, the TWAP oracle only applies to pairs on Uniswap v3, which means purely on-chain, while most DeFi Oracles offer data off-chain, such as prices of FAANG, oil, gold, etc.

Until someday in the future Uniswap decides to follow Vitalik’s proposal again, Uniswap is still a DEX, not an oracle project!

4. Conclusion

That’s it. Getting TWAPs from Uniswap v3 pools is an effortless and joyful experience. Big shout out to the team!

Other than the one mentioned already, I have two more articles related to Uniswap v3:

Feel free to check them out and leave any feedback down below. Until the next one!

Blockchain Enthusiast & Developer | https://github.com/tienshaoku