Skip to content

Latest commit

 

History

History
68 lines (43 loc) · 2.72 KB

076.md

File metadata and controls

68 lines (43 loc) · 2.72 KB

Deep Sepia Gazelle

Medium

TWAP price can be manipulated

Summary

Uniswap V3 pools utilize an observation mechanism to store historical price data, which is crucial for calculating Time-Weighted Average Prices (TWAP). The observation cardinality determines the number of historical data points the pool can store. The problem is that Uniswap V3 pools are initialized with an observation cardinality of 1.

Root Cause

The NumaOracle contract uses the TWAP oracle to calculate the price to convert ETH amounts to Numa tokens, convert Numa tokens back to ETH and to determine the lowest and highest prices. And the observation interval is set by the user as an input argument.

The problem is that Uniswap V3 pools are initialized with an observation cardinality of 1. The pool can only store the most recent observation. And historical price trends and volatility cannot be accurately assessed.

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

No response

Impact

TWAP requires multiple data points over a specified interval. With only one observation, TWAP calculations are highly unreliable. Moreover, lack of historical data makes the pool's price more susceptible to short-term fluctuations. In that way the TWAP can be easily manipulated due to reliance on a single data point, the price is returned direct for the spot price.

PoC

The NumaOracle::getV3SqrtPriceAvg calls the function IUniswapV3Pool.observe to retrieve and calculate the TWAP price:

function getV3SqrtPriceAvg(
    address _uniswapV3Pool,
    uint32 _interval
) public view returns (uint160) {
    require(_interval > 0, "interval cannot be zero");
    //Returns TWAP prices for short and long intervals
    uint32[] memory secondsAgo = new uint32[](2);
    secondsAgo[0] = _interval; // from (before)
    secondsAgo[1] = 0; // to (now)

    (int56[] memory tickCumulatives, ) = IUniswapV3Pool(_uniswapV3Pool)
@>      .observe(secondsAgo);

    // tick(imprecise as it's an integer) to sqrtPriceX96
    return
        TickMath.getSqrtRatioAtTick(
            int24(
                (tickCumulatives[1] - tickCumulatives[0]) /
                    int56(int32(_interval))
            )
        );
}

But Uniswap V3 pools are initialized with an observationCardinality equals to 1 and therefore the pool will return only the most recent observation.

Mitigation

The protocol should call the IUniswapV3Pool.increaseObservationCardinalityNext(uint16 observationCardinalityNext) function with a value high enough to cover the observation interval.