# How the TWAP Oracle in Uniswap v2 Works

## What exactly is “price” in Uniswap?

Suppose we have 1 Ether and 2,000 USDC in a pool. This implies that the price of Ether is 2,000 USDC. Specifically, the price of Ether is 2,000 USDC / 1 Ether (ignoring decimals).

More generally, the price of an asset, in terms of the price of the other asset in the pair, is a ratio where the “asset you care about” is in the denominator.

$$
\mathsf{price}(\text{foo})=\frac{\mathsf{reserve}(\text{foo})}{\mathsf{reserve}(\text{bar})}
$$

In the example above, it is saying “how may **bars** do you need to pay to get one **foo**” (ignoring fees).

## Price is a ratio

Because price is a ratio, they need to be stored with a data type which has decimal points (which Solidity types do not have by default).

That is, we say Ethereum is 2000 and USDC (in price of Ethereum) is 0.0005 (this is ignoring decimals of both assets).

Uniswap uses a fixed point number with 112 bits of precision on each side of the decimal, this takes up a total of 224 bits, and when packed with a 32 bit number, it uses up a single slot.

## Oracle definition

An oracle in computer science terms is a “source of truth.” A price oracle is a source of prices. Uniswap has an implied price when holding two assets, and other smart contracts can use this as a price oracle.

The intended users of the oracle are other smart contracts, since other smart contracts can easily communicate with Uniswap to determine the price, but getting price data from an off-chain exchange would be a lot harder.

However, just taking the ratio of the balances to get the current price isn’t safe.

## Motivation behind TWAP

Measuring an instantaneous snapshot of assets in the pool leaves an opportunity for flash loan attacks. That is, someone can make a huge trade using a flash loan to cause a temporary dramatic shift in the price, then take advantage of another smart contract that uses this price to make decisions.

The Uniswap V2 oracle defends against this in two ways:

- It provides a mechanisms for consumers of the price (usually smart contracts) to take the average a previous time period (decided by the user). This means an attacker has to constantly manipulate the price for several blocks, which is a lot more costly than using a flash loan.
- It doesn’t incorporate the current balance into the oracle calculation

This should not give the impression that oracles which use a moving average are immune to price manipulation attacks. If the asset does not have much liquidity, or the time window of taking the average is not sufficiently large, then a well-resourced attacker can still prop up the price (or suppress the price) long enough to manipulate the average price at the time of measurement.

## How TWAP works

A TWAP (Time Weighted Average Price) is like a simple moving average except that times where the price “stayed the same” longer get more weight — a TWAP weights price by **how long** the price stays at a certain level.

- Over the last day, the price of an asset was \$10 for the first 12 hours and \$11 for the second 12 hours. The average price is the same as the time weighted average price: \$10.5.
- Over the last day, the price of an asset was \$10 for the first 23 hours and \$11 for the most recent one. The expected average price should be closer to \$10 than \$11, but it will still be in between those values. Specifically, it will be ($10
* 23 + \$11 * 1) / 24 = \$10.0417 - Over the last day, the price of an asset was \$10 for the first hour, and \$11 for the most recent 23 hours. We expect the TWAP to be closer to \$11 than 10. Specifically, it will be ($10
* 1 + \$11 * 23) / 24 = \$10.9583

In general, the TWAP formula is

$$
\text{time-weighted average price} = \frac{P_1T_1+P_2T_2+\dots+P_nT_n}{\sum_{i=1}^nT_i}
$$

Here T is a duration, not a timestamp. That is, how long the price stayed at that level.

## Uniswap V2 does not store lookback or the denominator

In our example above, we only looked at prices for the last 24 hours, but what if you care about prices for the last hour, week, or some other interval? Uniswap of course cannot store every look back that someone might be interested, and there also isn’t a good way to consistently snapshot the price as someone would have to pay for the gas.

The solution is that Uniswap only stores the numerator of values — every time a change is the liquidity ratio happens (mint, burn, swap, or sync are called), it records the new price and **how long the previous price lasted.**

The variables `price0Cumulativelast`

and `price1CumulativeLast`

are public, so an interested party needs to snapshot them.

**But this is an important point you should always remember **`price0CumulativeLast`

and `price1CumulativeLast`

are only updated on lines 79 and 80 in the code above (orange circle), and they can only increase until they overflow. There is no mechanism make them “go down.” They always increase with every call to `_update`

. This means they accumulate prices ever since the pool is launched, which could be a very long time.

## Limiting the lookback window

Clearly, we are generally not interested in the average price since the pool came into existence. We only want to look back a certain amount of time (1 hour, 1 day, etc).

Here is the TWAP formula again.

$$\text{all time TWAP price}=\frac{P_1T_1+P_2T_2+P_3T_3+P_4T_4+P_5T_5+P_6T_6}{\sum T}$$

If we are only interested in prices since T4, then we want to be doing the following

$$\text{TWAP price since T4}=\frac{P_4T_4+P_5T_5+P_6T_6}{T_4+T_5+T_6}$$

How do we accomplish this with code? Since the `price0Cumulativelast`

keeps recording

$$\text{price0CumulativeLast}=P_1T_1+P_2T_2+P_3T_3+P_4T_4+P_5T_5+P_6T_6$$

We need a way to isolate the parts we care about. Consider the following

$$\text{RecentWindow}=\text{price0CumulativeLast}-\text{UpToTime3}$$

If we snapshot the price at the end of $T_3$, we get the value `UpToTime3`

. If we wait until $T_6$ finishes, then we do `price0Cumulativelast - UpToTime3then`

we will get the cumulative prices of only the recent window. If we divide that by the duration of the RecentWindow $(T_4 + T_5 + T_6)$, then we get the TWAP price of the recent window.

Graphically, this is what is what we are doing with the price accumulator.

## Only calculating the last 1 hour TWAP in Solidity

If we want a 1 hour TWAP, we need to **anticipate** that we will need a snapshot of the accumulator one hour from now. So we need to access the public variable `price0CumulativeLast`

and the public function `getReserves()`

to get the last update time, and snapshot those values. (See the `snapshot()`

function below).

After at least 1 hour has passed, we can call `getOneHourPrice()`

and we will access the newest value of `price0CumulativeLast`

from Uniswap V2.

Since the time we snapshotted the old price, Uniswap has been updating the accumulator

$$P_iT_i+P_{i+1}T_{i+1}+…+P_nT_n$$

The following code is made as simple as possible for illustration purposes, production use is not advised.

```
contract OneHourOracle {
using UQ112x112 for uint224; // requires importing UQ112x112
IUniswapV2Pair uniswapV2pair;
UQ112x112 snapshotPrice0Cumulative;
uint32 lastSnapshotTime;
function getTimeElapsed() internal view returns (uint32 t) {
unchecked {
t = uint32(block.timestamp % 2**32) - lastSnapshotTime;
}
}
function snapshot() public returns (UQ112x112 twapPrice) {
require(getTimeElapsed() >= 1 hours, "snapshot is not stale");
// we don't use the reserves, just need the last timestamp update
( , , lastSnapshotTime) = uniswapV2pair.getReserves();
snapshotPrice0Cumulative = uniswapV2pair.price0CumulativeLast;
}
function getOneHourPrice() public view returns (UQ112x112 price) {
require(getTimeElapsed() >= 1 hours, "snapshot not old enough");
require(getTimeElapsed() < 3 hours, "price is too stale");
uint256 recentPriceCumul = uniswapV2pair.price0CumulativeLast;
unchecked {
twapPrice = (recentPriceCumul - snapshotPrice0Cumulative) / timeElapsed;
}
}
}
```

### What if the last snapshot is over three hours ago?

Astute readers may note that the above contract will not be able to snapshot if the pair it is interacting with hasn’t had an interaction in the last three hours. The Uniswap V2 function `_update`

is called during `mint`

, `burn`

, and `swap`

, but none of those interactions happen, then `lastSnapshotTime`

will record a time from a while ago. The solution is for the oracle to call the sync function at the time it does a snapshot, as that will internally call `_update`

.

The sync function is screenshotted below.

## Why TWAP must track two ratios

The price of A with respect to B is simply A/B and vice versa. For example, if we have 2000 USDC in the pool (ignoring decimals), and 1 Ether, then the price of 1 Ether is simply 2000 USDC / 1 ETH.

The price of USDC, denominated in ETH, is simply that number with the numerator and denominator flipped.

However, we cannot just “invert” one of the prices to get the other when we are accumulating pricing. Consider the following. If our price accumulator starts at 2 and adds 3, we cannot just do one over the accumulator:

$$
\frac{1}{2+3}\neq\frac{1}{2}+\frac{1}{3}
$$

However, the prices are still “somewhat symmetric,” hence the choice of fixed point arithmetic representation must have the same capacity for the integers and for the decimals. If Eth is 1,000 times more “valuable” than a USDC, then USDC is 1,000 times “less valuable” than USDC. To store this accurately, the fixed point number should have the same size on both sides of the decimal, hence Uniswap’s choice of `u112x112`

.

## PriceCumulativeLast always increases until it overflows, then keeps going

Uniswap V2 was built before `Solidity 0.8.0`

, thus arithmetic overflowed and underflowed by default. Correct modern implementations of the price oracle need to use the `unchecked`

block to ensure everything overflows as expected.

Eventually, the `priceAccumulators`

and the block timestamp will overflow. In that case, the previous reserve will be higher than the new reserve. When the oracle computes the change in price, they will get a negative value. However, this won’t matter due to the rules of modular arithmetic.

To make things simple let’s use an imaginary unsigned integers that overflow at 100.

We snapshot the `priceAccumulator`

at `80`

and a few transactions/blocks later the `priceAccumulator`

goes to `110`

, but it overflows to `10`

. We subtract `80`

from `10`

, which gives `-70`

. But the value is stored as an unsigned integer, so it gives `-70 mod(100)`

which is `30`

. That’s the same result we would expect if it didn’t overflow (`110-80=30`

).

This is true of all overflow boundaries, not just `100`

in our example. Overflowing the `timestamp`

or `priceAccumulator`

does not cause issues because of how modular arithmetic works.

### Overflowing the timestamp

The same thing happens when we overflow the `timestamp`

. Because we are using a `uint32`

to represent it, there won’t be any negative numbers. Again, let’s assume we overflow at `100`

for the sake of simplicity. If we snapshot at time `98`

and consult the price oracle at time `4`

, then `6`

seconds have passed. `4 - 98 % 100 = 6`

, as expected.

## Learn more with RareSkills

This material is part of our advanced Solidity Bootcamp. Please see the program to learn more.

*Originally Published November 3, 2023*