All articles
The BuildMay 12, 20269 min read

UQ112x112: How Uniswap V2's TWAP Math Actually Works

Uniswap V2's UQ112x112 fixed-point format encodes prices as 224-bit values for the TWAP oracle. Why 112 integer + 112 fractional, and how the math preserves precision.

By Carlos (Bloqarl)

TL;DR

  • UQ112x112 is Uniswap V2's custom fixed-point arithmetic format, used by the TWAP oracle.
  • encode(y) = y * 2**112. The result has 112 bits dedicated to the integer part and 112 bits to the fractional part. Fits in uint224.
  • uqdiv(uq, x) divides a UQ112x112 value by a uint112, preserving fractional precision. This is what lets the oracle compute reserve1 / reserve0 ratios without losing information.
  • The 256-bit storage budget is divided deliberately: 112 + 112 = 224 bits for the value, leaving 32 bits of headroom for accumulator overflow during the cumulative-price multiplication.
  • If you fork Uniswap V2's TWAP without understanding UQ112x112, the most common bug is silent precision loss: you compute the same average price on different paths and they disagree.

Why this matters

Solidity has no native fractional types. uint256, int256, and friends are all integers. But DeFi math is full of fractional values: prices, ratios, rates. Every protocol that handles prices on-chain implements fixed-point arithmetic in some form.

UQ112x112 is the form Uniswap V2 picked for its TWAP oracle. The choice is non-obvious; the alternatives include UQ128x128, UQ64x96 (Uniswap V3), UD60x18 (PRBMath), or arbitrary-precision libraries. Each picks a different trade-off between precision, range, and gas.

Understanding UQ112x112 specifically lets you:

  • Audit the TWAP oracle in any Uniswap V2 fork without confusing yourself.
  • Modify the oracle parameters without breaking the math.
  • Spot precision bugs in code that integrates Uniswap V2 prices.

If you're building on top of a Uniswap V2 pair (lending against its tokens, computing positions, doing slippage analysis), the UQ112x112 representation is what the pair publishes. Treating it as opaque is the route to silent precision bugs.

What fixed-point arithmetic is

Fixed-point arithmetic represents fractional numbers as integers, with an implicit scaling factor.

A simple example: to represent the number 1.5 as an integer, scale by 100. The integer is 150. To get 1.5 back, divide by 100. To add 1.5 + 2.7 (= 4.2), add 150 + 270 = 420, then "divide by 100" mentally to read 4.2.

The scaling factor is the "decimal point position". With scale 100, you have 2 fractional digits. With scale 1e18, you have 18 fractional digits, which is what most DeFi math uses.

UQ112x112 uses a binary scaling factor instead of a decimal one. Specifically: scale by 2**112. So 1.5 is represented as 1.5 * 2**112 = 7.79e33 (a 113-bit number). To compute, you do integer arithmetic on these scaled values; to read, you divide by 2**112 (typically by bit-shifting right 112 places).

The "UQ" in UQ112x112 stands for "Unsigned Q-format". The "112x112" means 112 integer bits + 112 fractional bits. Total: 224 bits, plus headroom.

How encode, decode, and uqdiv work

The full UQ112x112 library in Uniswap V2 is small enough to read in one sitting:

library UQ112x112 {
    uint224 constant Q112 = 2**112;

    // encode a uint112 as a UQ112.112
    function encode(uint112 y) internal pure returns (uint224 z) {
        z = uint224(y) * Q112; // never overflows
    }

    // divide a UQ112x112 by a uint112, returning a UQ112x112
    function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
        z = x / uint224(y);
    }
}

That's the entire library. Three pieces:

encode

Multiply a uint112 by 2**112. The result fits in uint224. The "never overflows" comment is correct: (2**112 - 1) * 2**112 is roughly 2**224 - 2**112, well within uint224's 2**224 - 1 capacity.

What you've done is shift the integer left by 112 binary places. The original value is now sitting in the upper 112 bits of a 224-bit number, with all zeros in the lower 112 bits. That's a UQ112x112 representation of the same number.

uqdiv

Divide a UQ112x112 by a uint112. Critically, the divisor is a regular integer, NOT another UQ112x112.

This works because: if x = a * 2**112 (UQ112x112 of a), then x / b = (a * 2**112) / b = (a / b) * 2**112. So the result is a/b in UQ112x112 form. The fractional bits that would be lost in plain integer division are preserved by the 2**112 scaling.

In Uniswap V2's TWAP, uqdiv is called as UQ112x112.encode(reserve1).uqdiv(reserve0), computing reserve1 / reserve0 with 112 bits of fractional precision.

decode

There's no explicit decode function. To read the value, you bit-shift right by 112: x >> 112 gives the integer part. The fractional part is x & ((1 << 112) - 1). In practice, downstream code rarely needs the human-readable value; it does math directly on UQ112x112 values.

The 256-bit budget

Uniswap V2's TWAP accumulator is a uint256. Why use UQ112x112 (224 bits) for the per-update price, leaving 32 bits unused?

The 32 unused bits are headroom for the accumulator's growth.

When the oracle accumulates price * time, the multiplication can produce values larger than uint224. Specifically:

  • Per-update price (UQ112x112): up to 2**224 - 1 ≈ 2.7e67.
  • timeElapsed (uint32): up to 2**32 - 1 ≈ 4.3e9.
  • Product (per-update accumulation): up to (2**224 - 1) * (2**32 - 1)2**256 - 2**224 - 2**32.

That product fits in uint256 exactly. The 32 bits of headroom in the accumulator's data type (256 - 224 = 32) match the maximum width of timeElapsed, making the multiplication overflow-safe within a single update.

But over many updates, the cumulative accumulator can grow past uint256. That's deliberate: the TWAP only uses differences between two cumulative snapshots, and modular subtraction handles wraparound. We covered this in the dual-arithmetic article. The accumulator is allowed to overflow; the per-update math is not.

The 112+112+32 split is the unique partitioning that:

  • Fits the largest plausible reserve (uint112: ~5e33, more than enough for any token).
  • Provides 112 bits of fractional precision (more than needed for most prices, but useful for very small ratios like a long-tail token vs ETH).
  • Leaves exactly the right headroom for timeElapsed (uint32) multiplication without overflow within an update.

If you change any one of these widths, the others have to change. UQ112x112 is not arbitrary; it's the consequence of three independent constraints.

How the TWAP oracle uses UQ112x112

The TWAP oracle is built on cumulative prices. Each time _update() runs (during a mint, burn, or swap), the oracle:

  1. Computes per-update price as UQ112x112.encode(reserve1).uqdiv(reserve0). This is the spot price ratio reserve1 / reserve0 in UQ112x112 form.
  2. Multiplies by timeElapsed (the seconds since the last update).
  3. Adds to price0CumulativeLast. This accumulator tracks "the integral of price0 over time".
  4. Same for price1CumulativeLast (the inverse direction).

The cumulative accumulator grows monotonically (modulo uint256 overflow). To compute the average price between time T1 and T2:

avgPrice = (cumulative[T2] - cumulative[T1]) / (T2 - T1)

The result is in UQ112x112 form, representing the average of reserve1 / reserve0 over the window. Downstream consumers can decode it, compare it to expected values, use it as a manipulation-resistant price feed.

The whole design relies on UQ112x112 having enough fractional precision that small price changes are not rounded away. For a price like 0.000123 (a common ratio for low-cap tokens vs ETH), UQ112x112 stores 0.000123 * 2**1126.39e29, which has many bits of fractional precision still available for finer movements.

If you used a smaller fractional width (say UQ112x64), prices below ~2^-64 would round to zero. Many real token pairs would lose all price signal.

Common pitfalls when porting or modifying

Mistake 1: copying the library to a different uint width

A fork wants to support reserves larger than uint112. They change the library to UQ128x128 (128 integer + 128 fractional). Now the cumulative accumulator can't fit in uint256: 2**256 + 2**32 overflows. The oracle reverts on every update.

Fix requires also changing the cumulative accumulator to uint512 (or splitting into two uint256s with manual carry handling).

Mistake 2: dividing two UQ112x112 values directly

uqdiv divides UQ112x112 by uint112, NOT by another UQ112x112. If you wrote (reserve1.encode()).uqdiv(reserve0.encode()), the math is wrong: dividing two UQ112x112 values gives a uint224 / 2**112 = uint112, which is a plain integer, not a UQ112x112.

The correct pattern is UQ112x112.encode(reserve1).uqdiv(reserve0) (encode the numerator, divide by the raw uint112 denominator).

Mistake 3: integer overflow in user code

A consumer of the TWAP oracle reads cumulative[T1] and cumulative[T2], then subtracts. If they cast the cumulative values to int256 (signed) for the subtraction, they can introduce sign bugs when the unsigned cumulative is in the upper half of uint256 (interpreted as negative when cast). Use unchecked unsigned subtraction; modular arithmetic handles the wrap correctly.

Mistake 4: skipping the timeElapsed check

Uniswap V2 only updates the cumulative when timeElapsed > 0 (and reserves are non-zero). If a fork removes this check, multiple updates within the same block compute timeElapsed = 0 and produce no accumulator change, which is fine. But if a fork inserts an update that uses timeElapsed as a divisor somewhere else, the divide-by-zero crashes the contract.

Related questions

Why not use OpenZeppelin's SafeMath or PRBMath? Both exist, both work. Uniswap V2 wrote UQ112x112 inline because the library wanted minimal dependencies and the TWAP's specific use case is narrow enough that a custom 16-line library suffices. For protocols using prices in many places, PRBMath's UD60x18 is a better general-purpose choice.

What's the difference between UQ112x112 and Q112.112? Q-format is the umbrella term. UQ means unsigned Q-format. Q with no prefix can be either signed or unsigned depending on context. The numbers (112x112, or 112.112) specify the integer-bits and fractional-bits widths.

How does Uniswap V3's tick math relate? V3 uses a different fixed-point format (UQ64x96) and does most price math in tick space (logarithmic) rather than direct UQ space. The TWAP design is fundamentally different (oracle observations as a ring buffer instead of a single accumulator). The V2 walkthrough above doesn't transfer.

Is UQ112x112 vulnerable to manipulation in the same way as spot prices? No, that's the whole point of TWAP. The cumulative accumulator integrates over time; a single-block manipulation is dampened by the time window. But TWAP is not free of manipulation; long enough windows reduce manipulation, but increase staleness. Our oracle zero-price article covers the related Compound V2 oracle design.

What's the gas cost of UQ112x112 operations? Roughly free. encode is one multiplication. uqdiv is one division. Both are trivial compared to the storage operations they accompany.

Where to see this in Academy

The TWAP oracle is implemented in section 04 of the Uniswap V2 reconstruction (pair-oracle). The Academy's test suite validates UQ112x112 math directly:

  • encode(y) * 2**112 == y * 2**224 (the encode is correct).
  • uqdiv(encode(a), b) * b ≈ encode(a) (preserving precision under round-trip).
  • The cumulative accumulator wraps cleanly around uint256.
  • Two snapshot subtractions produce the correct elapsed price even across overflow boundaries.

Implementing this from scratch is one of the trickier sections for engineers new to fixed-point arithmetic. The test suite keeps you honest. When the tests pass, you understand UQ112x112 in a way that reading the source can't quite teach.

Tagged

Uniswap V2TWAP OracleFixed-Point Math