Skip to content
Back to skills

Uniswap V3 Oracle

ASecurity

This skill should be used when the user asks about "oracle", "TWAP", "time-weighted average price", "observations", "observe", "price oracle", "tick cumulative", or needs to understand the on-chain oracle system.

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 10, 2026
code-qualitygoapi

Works with

  • api

Security analysis

A100/100

Scanned September 10, 2026

npx -y skills add cyotee/indexedex --skill uniswap-v3-oracle --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Uniswap V3 Oracle?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Uniswap V3 Oracle
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/cyotee-uniswap-v3-oracle/badge)](https://www.skillsdirectory.com/skills/cyotee-uniswap-v3-oracle)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Uniswap V3 Oracle
description: This skill should be used when the user asks about "oracle", "TWAP", "time-weighted average price", "observations", "observe", "price oracle", "tick cumulative", or needs to understand the on-chain oracle system.
version: 0.1.0
---

# Uniswap V3 Oracle

## Overview

Uniswap V3 pools include a built-in oracle that stores historical price observations, enabling on-chain calculation of time-weighted average prices (TWAPs) without external oracle dependencies.

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                          ORACLE ARCHITECTURE                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Observation Ring Buffer (circular array):                                  │
│                                                                              │
│  ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐             │
│  │  0  │  1  │  2  │  3  │  4  │  5  │  6  │  7  │  8  │ ... │             │
│  └──┬──┴─────┴─────┴──┬──┴─────┴─────┴──┬──┴─────┴─────┴─────┘             │
│     │                 │                 │                                    │
│     │                 │                 └─ observationIndex (current)       │
│     │                 │                                                      │
│     │                 └─ Historical observation                              │
│     │                                                                        │
│     └─ Oldest observation (wraps around)                                     │
│                                                                              │
│  Each Observation:                                                          │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │  blockTimestamp: uint32           │ When this was recorded           │   │
│  │  tickCumulative: int56            │ Sum of tick × time               │   │
│  │  secondsPerLiquidityCumX128: u160 │ Sum of 1/liquidity × time        │   │
│  │  initialized: bool                │ Whether slot is used             │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  TWAP Calculation:                                                          │
│  ─────────────────                                                          │
│  twapTick = (tickCumulative[now] - tickCumulative[ago]) / timeElapsed      │
│  twapPrice = 1.0001^twapTick                                                │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

## Observation Data Structure

```solidity
library Oracle {
    struct Observation {
        // Timestamp of this observation
        uint32 blockTimestamp;

        // Cumulative sum of tick values over time
        // tickCumulative[t] = ∑(tick[i] × duration[i]) for all i from 0 to t
        int56 tickCumulative;

        // Cumulative sum of 1/liquidity over time (for liquidity mining)
        // secondsPerLiquidity[t] = ∑(1/liquidity[i] × duration[i]) for all i from 0 to t
        uint160 secondsPerLiquidityCumulativeX128;

        // Whether this observation slot has been written
        bool initialized;
    }
}
```

## Oracle Initialization

```solidity
function initialize(uint32 time)
    internal
    returns (uint16 cardinality, uint16 cardinalityNext)
{
    observations[0] = Observation({
        blockTimestamp: time,
        tickCumulative: 0,
        secondsPerLiquidityCumulativeX128: 0,
        initialized: true
    });
    return (1, 1);
}
```

## Writing Observations

Observations are written at most once per block, during the first swap/mint/burn of each block.

```solidity
function write(
    Observation[65535] storage self,
    uint16 index,
    uint32 blockTimestamp,
    int24 tick,
    uint128 liquidity,
    uint16 cardinality,
    uint16 cardinalityNext
) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) {
    Observation memory last = self[index];

    // Only write once per block
    if (last.blockTimestamp == blockTimestamp) return (index, cardinality);

    // Grow cardinality if next is larger
    if (cardinalityNext > cardinality && index == (cardinality - 1)) {
        cardinalityUpdated = cardinalityNext;
    } else {
        cardinalityUpdated = cardinality;
    }

    // Calculate new index (wraps around)
    indexUpdated = (index + 1) % cardinalityUpdated;

    // Calculate time delta
    uint32 delta = blockTimestamp - last.blockTimestamp;

    // Write new observation
    self[indexUpdated] = Observation({
        blockTimestamp: blockTimestamp,
        tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)),
        secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +
            ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),
        initialized: true
    });
}
```

## Reading Observations (observe)

```solidity
/// @notice Returns the cumulative values at specific times ago
function observe(
    Observation[65535] storage self,
    uint32 time,
    uint32[] memory secondsAgos,
    int24 tick,
    uint16 index,
    uint128 liquidity,
    uint16 cardinality
) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) {
    require(cardinality > 0, 'I');

    tickCumulatives = new int56[](secondsAgos.length);
    secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length);

    for (uint256 i = 0; i < secondsAgos.length; i++) {
        (tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = observeSingle(
            self,
            time,
            secondsAgos[i],
            tick,
            index,
            liquidity,
            cardinality
        );
    }
}

function observeSingle(
    Observation[65535] storage self,
    uint32 time,
    uint32 secondsAgo,
    int24 tick,
    uint16 index,
    uint128 liquidity,
    uint16 cardinality
) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) {
    if (secondsAgo == 0) {
        // Current values
        Observation memory last = self[index];
        if (last.blockTimestamp != time) {
            // Extrapolate to current time
            uint32 delta = time - last.blockTimestamp;
            return (
                last.tickCumulative + int56(tick) * int56(uint56(delta)),
                last.secondsPerLiquidityCumulativeX128 +
                    ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1))
            );
        }
        return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128);
    }

    uint32 target = time - secondsAgo;

    // Binary search for surrounding observations
    (Observation memory beforeOrAt, Observation memory atOrAfter) =
        getSurroundingObservations(self, time, target, tick, index, liquidity, cardinality);

    if (target == beforeOrAt.blockTimestamp) {
        // Exact match
        return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128);
    } else if (target == atOrAfter.blockTimestamp) {
        // Exact match
        return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128);
    } else {
        // Interpolate between observations
        uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp;
        uint32 targetDelta = target - beforeOrAt.blockTimestamp;

        return (
            beforeOrAt.tickCumulative +
                ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / int56(uint56(observationTimeDelta))) *
                int56(uint56(targetDelta)),
            beforeOrAt.secondsPerLiquidityCumulativeX128 +
                uint160(
                    (uint256(
                        atOrAfter.secondsPerLiquidityCumulativeX128 -
                        beforeOrAt.secondsPerLiquidityCumulativeX128
                    ) * targetDelta) / observationTimeDelta
                )
        );
    }
}
```

## Growing Oracle Cardinality

By default, pools store only 1 observation. This can be expanded up to 65,535.

```solidity
/// @notice Prepares the oracle to store more observations
function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external lock {
    uint16 observationCardinalityNextOld = slot0.observationCardinalityNext;
    uint16 observationCardinalityNextNew = observations.grow(
        observationCardinalityNextOld,
        observationCardinalityNext
    );
    slot0.observationCardinalityNext = observationCardinalityNextNew;
    if (observationCardinalityNextOld != observationCardinalityNextNew)
        emit IncreaseObservationCardinalityNext(observationCardinalityNextOld, observationCardinalityNextNew);
}

function grow(
    Observation[65535] storage self,
    uint16 current,
    uint16 next
) internal returns (uint16) {
    require(current > 0, 'I');
    if (next <= current) return current;

    // Initialize new slots
    for (uint16 i = current; i < next; i++) {
        self[i].blockTimestamp = 1;  // Mark as initialized but empty
    }
    return next;
}
```

## TWAP Calculation

### Basic TWAP

```solidity
// Example: Get TWAP over last 10 minutes (600 seconds)
function getTwap(IUniswapV3Pool pool, uint32 twapInterval)
    external view
    returns (int24 arithmeticMeanTick)
{
    uint32[] memory secondsAgos = new uint32[](2);
    secondsAgos[0] = twapInterval;  // Start time
    secondsAgos[1] = 0;              // End time (now)

    (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);

    int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
    arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(twapInterval)));

    // Handle rounding for negative ticks
    if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int56(uint56(twapInterval)) != 0)) {
        arithmeticMeanTick--;
    }
}
```

### OracleLibrary (Periphery)

```solidity
library OracleLibrary {
    /// @notice Calculates time-weighted mean tick
    function consult(address pool, uint32 secondsAgo)
        internal view
        returns (int24 arithmeticMeanTick, uint128 harmonicMeanLiquidity)
    {
        require(secondsAgo != 0, 'BP');

        uint32[] memory secondsAgos = new uint32[](2);
        secondsAgos[0] = secondsAgo;
        secondsAgos[1] = 0;

        (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) =
            IUniswapV3Pool(pool).observe(secondsAgos);

        int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
        uint160 secondsPerLiquidityCumulativesDelta =
            secondsPerLiquidityCumulativeX128s[1] - secondsPerLiquidityCumulativeX128s[0];

        arithmeticMeanTick = int24(tickCumulativesDelta / int56(uint56(secondsAgo)));
        if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int56(uint56(secondsAgo)) != 0))
            arithmeticMeanTick--;

        // Harmonic mean liquidity (useful for liquidity mining)
        uint192 secondsAgoX160 = uint192(secondsAgo) * type(uint160).max;
        harmonicMeanLiquidity = uint128(secondsAgoX160 / (uint192(secondsPerLiquidityCumulativesDelta) << 32));
    }

    /// @notice Given a tick and a token amount, calculates the equivalent in the other token
    function getQuoteAtTick(
        int24 tick,
        uint128 baseAmount,
        address baseToken,
        address quoteToken
    ) internal pure returns (uint256 quoteAmount) {
        uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);

        if (sqrtRatioX96 <= type(uint128).max) {
            uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
            quoteAmount = baseToken < quoteToken
                ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192)
                : FullMath.mulDiv(1 << 192, baseAmount, ratioX192);
        } else {
            uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
            quoteAmount = baseToken < quoteToken
                ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128)
                : FullMath.mulDiv(1 << 128, baseAmount, ratioX128);
        }
    }

    /// @notice Get oldest available observation timestamp
    function getOldestObservationSecondsAgo(address pool)
        internal view
        returns (uint32 secondsAgo)
    {
        (, , uint16 observationIndex, uint16 observationCardinality, , , ) =
            IUniswapV3Pool(pool).slot0();

        // Oldest observation is at index + 1 (wrapped)
        uint16 oldestIndex = (observationIndex + 1) % observationCardinality;
        (uint32 oldestTimestamp, , , bool initialized) =
            IUniswapV3Pool(pool).observations(oldestIndex);

        if (initialized) {
            secondsAgo = uint32(block.timestamp) - oldestTimestamp;
        } else {
            // Array not fully filled, oldest is at index 0
            (uint32 timestamp, , , ) = IUniswapV3Pool(pool).observations(0);
            secondsAgo = uint32(block.timestamp) - timestamp;
        }
    }
}
```

## Snapshot Cumulatives Inside

For tracking fees/rewards within a position's tick range.

```solidity
function snapshotCumulativesInside(int24 tickLower, int24 tickUpper)
    external view
    returns (
        int56 tickCumulativeInside,
        uint160 secondsPerLiquidityInsideX128,
        uint32 secondsInside
    )
{
    checkTicks(tickLower, tickUpper);

    int56 tickCumulativeLower;
    int56 tickCumulativeUpper;
    uint160 secondsPerLiquidityOutsideLowerX128;
    uint160 secondsPerLiquidityOutsideUpperX128;
    uint32 secondsOutsideLower;
    uint32 secondsOutsideUpper;

    {
        Tick.Info storage lower = ticks[tickLower];
        Tick.Info storage upper = ticks[tickUpper];

        // Get values from tick storage
        tickCumulativeLower = lower.tickCumulativeOutside;
        tickCumulativeUpper = upper.tickCumulativeOutside;
        secondsPerLiquidityOutsideLowerX128 = lower.secondsPerLiquidityOutsideX128;
        secondsPerLiquidityOutsideUpperX128 = upper.secondsPerLiquidityOutsideX128;
        secondsOutsideLower = lower.secondsOutside;
        secondsOutsideUpper = upper.secondsOutside;
    }

    Slot0 memory _slot0 = slot0;

    if (_slot0.tick < tickLower) {
        // Current price below range
        return (
            tickCumulativeLower - tickCumulativeUpper,
            secondsPerLiquidityOutsideLowerX128 - secondsPerLiquidityOutsideUpperX128,
            secondsOutsideLower - secondsOutsideUpper
        );
    } else if (_slot0.tick < tickUpper) {
        // Current price inside range
        uint32 time = _blockTimestamp();
        (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
            observations.observeSingle(time, 0, _slot0.tick, _slot0.observationIndex, liquidity, _slot0.observationCardinality);

        return (
            tickCumulative - tickCumulativeLower - tickCumulativeUpper,
            secondsPerLiquidityCumulativeX128 - secondsPerLiquidityOutsideLowerX128 - secondsPerLiquidityOutsideUpperX128,
            time - secondsOutsideLower - secondsOutsideUpper
        );
    } else {
        // Current price above range
        return (
            tickCumulativeUpper - tickCumulativeLower,
            secondsPerLiquidityOutsideUpperX128 - secondsPerLiquidityOutsideLowerX128,
            secondsOutsideUpper - secondsOutsideLower
        );
    }
}
```

## Oracle Best Practices

1. **Cardinality**: Expand cardinality before needing historical data
2. **TWAP Window**: Use longer windows (10-30 min) to resist manipulation
3. **Flash Loan Resistance**: TWAP is resistant to single-block manipulation
4. **Gas Cost**: Longer history = more binary search steps

```
Cardinality vs History Trade-off:
┌─────────────┬──────────────────────────────────────┐
│ Cardinality │ Approximate History (1 block/slot)   │
├─────────────┼──────────────────────────────────────┤
│     1       │ Current block only                   │
│    10       │ ~2 minutes                           │
│   100       │ ~20 minutes                          │
│  1000       │ ~3.3 hours                           │
│ 65535       │ ~9 days                              │
└─────────────┴──────────────────────────────────────┘
```

## Events

```solidity
event IncreaseObservationCardinalityNext(
    uint16 observationCardinalityNextOld,
    uint16 observationCardinalityNextNew
);
```

## Reference Files

### v3-core
- `contracts/libraries/Oracle.sol` - Observation storage and interpolation
- `contracts/UniswapV3Pool.sol` - observe() and snapshotCumulativesInside()

### v3-periphery
- `contracts/libraries/OracleLibrary.sol` - TWAP calculation helpers

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…