Price Oracle
Uniswap V2’s price oracle mechanism is designed to provide highly decentralized, manipulation-resistant on-chain price feeds by shifting from spot price lookups to Time-Weighted Average Prices (TWAP).
Uniswap V1 Issue
Section titled “Uniswap V1 Issue”Uniswap V1 normal AMM behavior: if you buy a lot from a thin pool, the reserve ratio changes and the quoted spot price moves sharply.
The problem appears when another contract treats that instantly movable spot price as a reliable oracle for collateral valuation, borrowing limits, minting, or liquidation decisions.
The victim is the protocol that trusted the manipulated price, not the Uniswap V1 pool simply for existing and repricing after a big trade.
Attack pattern: manipulate Uniswap price (especially in thin markets), call the victim contract while the price is distorted, then profit from the victim’s bad accounting.
Attack Example
Section titled “Attack Example”REP/ETH Oracle Manipulation (from uniswapV2 white-paper)
| Step | Action | Protocol Touched | Key Numbers | Pair Price (REP/ETH) | Why It Matters |
|---|---|---|---|---|---|
| 1 | Check REP/ETH spot price | Uniswap V1 + Kyber | Pool holds 8,856.10 REP | 0.057621 ETH per REP | This is the honest baseline price before any manipulation |
| 2 | Buy 7,970.49 REP from pool | Uniswap V1 | 7,970.49 REP bought (~90% of pool liquidity) | Price starts rising sharply | Draining 90% of REP reserves forces the constant-product formula to reprice drastically |
| 3 | Check REP/ETH spot price again | Uniswap V1 + Kyber | Slippage-adjusted quote = 5.486688 ETH for 10 REP | 5.656380 ETH per REP (manipulated) | Price is now ~98.2x inflated vs. step 1 — the oracle sees a fake market |
| 4 | Deposit REP as collateral | bZx (lending pool) | collateral_required = 702.265284; interest_required = 205.433213 | 5.656380 ETH per REP (manipulated) | bZx reads the Kyber/Uniswap price and accepts REP as if it were worth 98x more than reality |
| 5 | Borrow ETH against REP collateral | bZx (lending pool) | Loan sized based on inflated collateral value | 5.656380 ETH per REP (manipulated) | Protocol issues far more ETH than REP is truly worth — now undercollateralized |
| 6 | Unwind: sell REP back into pool | Uniswap V1 | REP returned to pool, reserves normalize | ~0.057621 ETH per REP (back to normal) | The manipulated price only needed to exist for the duration of steps 3–5 |
| 7 | Pocket the profit | — | Final profit = 2,425.71 ETH | — | Attacker borrowed real ETH against fake collateral value and keeps the difference |
Uniswap V2 Solution
Section titled “Uniswap V2 Solution”V2 mitigates this by
- Uniswap v2 measures the price before the first trade of each block, which is effectively the previous block’s closing price.
- This makes one-transaction price manipulation much less effective against the oracle.
- To manipulate the measured price, two things must happen:
- The attacker executes a bad trade near the end of one block, taking on slippage and fees.
- the first state-changing interaction with the pair in the next block updates the oracle price using the manipulated carried-over reserves carried over from the previous block.
- The attacker would prefer to commit the first tx in the next block, which is highly unlikely unless they control ordering or consecutive blocks,
- During that time, arbitrageurs can trade against the mispricing.
- (Optional) The attacker may then exploit a victim protocol.
- (Optional) The attacker may then unwind / recovery trade
- (Optional) During that extended time, arbitrageurs can trade against the mispricing.
- To manipulate the measured price, two things must happen:
- This makes one-transaction price manipulation much less effective against the oracle.
- Shifting from spot price lookups to Time-Weighted Average Prices (TWAP)
- Smooths short-lived spikes, so a brief manipulation usually affects the average much less than it affects the instantaneous spot price.
Update method
Section titled “Update method”Let’s see how it achieves those defenses
// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:22:24uint112 private reserve0; // uses single storage slot, accessible via getReservesuint112 private reserve1; // uses single storage slot, accessible via getReservesuint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
// contracts/uniswap-v2/v2-core/contracts/UniswapV2Pair.sol:73:86function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1);}Update per block
Section titled “Update per block”- Every time an interaction (mint, swap, burn) occurs
_update()is called - Contract updates its internal accumulator on the first interaction in block
- changes the reserves immediately on block ,
- price is added into the oracle accumulator in block based on reserves on block
block.timestampincreases when block increases- it updates its price only when
block.timestampincreases - blockTimestamp stores a cropped version of
block.timestamp(uint256), truncated to uint32 for storage packing efficiency.- seconds after the Unix epoch (January 1, 1970)
- seconds after epoch = February 7, 2106, at 06:28:16 UTC
- Contract does not store historical prices
- Contract tracks a single cumulative sum of the price for every second in the entire history of the contract
- Each pair tracks
price0CumulativeLastandprice1CumulativeLast - There might be cases when a block did not have any interaction; therefore, the price is not updated
Fixed point binary numbers
Section titled “Fixed point binary numbers”Reference Fixed Point Binary Numbers
Uses UQ112.112 to store TWAP
| Property | Value |
|---|---|
| Total bits | 224 |
| Min value | |
| Max value | |
| Resolution |
This is an enormous format
- it gives you a range on the scale of
- while simultaneously resolving differences as small as
Storage optimization
Section titled “Storage optimization”For reserves, Uniswap stores reserve0 and reserve1 as uint112 plus blockTimestampLast as uint32 in one packed slot.
Definition
Section titled “Definition” is a duration from block N to the first interaction from block N+1 (timeElapsed)
Retrieval
Section titled “Retrieval”Retrieving TWAP for block M formula:
= prior block, = target block
Notes:
- blocks (30min): gives manipulation-resistant average
- : Modulo handles Solidity’s 32-bit timestamp wrapping
- : Decoding converts fixed-point to decimal