// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {ITwapSource} from "../OracleHub.sol"; /// @dev Minimal Uniswap v3 pool interface (observe + slot0 + liquidity). interface IUniswapV3Pool { function observe(uint32[] calldata secondsAgos) external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); function token0() external view returns (address); function token1() external view returns (address); function liquidity() external view returns (uint128); } interface IERC20Decimals { function decimals() external view returns (uint8); } /// @title UniV3TwapReader — long-window TWAP for RAIN/ARB (White Paper v3.2 §9) /// @notice Computes a 30–60 minute arithmetic-mean-tick TWAP from a Uniswap v3 pool, /// then converts to WAD USD via a stable-denominated pair (e.g. RAIN/USDC). /// Long windows defeat single-block manipulation of thin-liquidity assets; /// the OracleHub's circuit breaker adds the second layer of defense. /// @dev Immutable configuration — pool, token orientation, and window are fixed at /// deployment. To change the window or pool, deploy a new reader and point the /// OracleHub at it through the timelock. contract UniV3TwapReader is ITwapSource { error WindowTooShort(); error PoolLiquidityTooLow(); IUniswapV3Pool public immutable pool; /// @notice true if the asset being priced is token0 (quote = token1) bool public immutable assetIsToken0; uint32 public immutable twapWindow; // seconds, enforce >= 30 min (WP §9) uint128 public immutable minLiquidity; // sanity floor against near-empty pools uint256 public immutable assetScale; // 10**asset.decimals() uint256 public immutable quoteScale; // 10**quote.decimals() constructor(address _pool, address asset, uint32 _window, uint128 _minLiquidity) { if (_window < 30 minutes) revert WindowTooShort(); pool = IUniswapV3Pool(_pool); twapWindow = _window; minLiquidity = _minLiquidity; address t0 = pool.token0(); address t1 = pool.token1(); assetIsToken0 = (asset == t0); address quote = assetIsToken0 ? t1 : t0; assetScale = 10 ** IERC20Decimals(asset).decimals(); quoteScale = 10 ** IERC20Decimals(quote).decimals(); } /// @inheritdoc ITwapSource /// @return WAD price of the asset in quote (stable) terms. function twapPrice() external view returns (uint256) { if (pool.liquidity() < minLiquidity) revert PoolLiquidityTooLow(); uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = twapWindow; secondsAgos[1] = 0; (int56[] memory tickCumulatives,) = pool.observe(secondsAgos); int56 delta = tickCumulatives[1] - tickCumulatives[0]; int24 meanTick = int24(delta / int56(uint56(twapWindow))); // round toward negative infinity (canonical Uniswap OracleLibrary behavior) if (delta < 0 && (delta % int56(uint56(twapWindow)) != 0)) meanTick--; uint160 sqrtPriceX96 = _getSqrtRatioAtTick(meanTick); // priceX192 = (sqrtPriceX96)^2 gives token1/token0 in Q192 uint256 priceX192 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96); if (assetIsToken0) { // token1 per token0, normalized to WAD: p * 1e18 * assetScale / (2^192 * quoteScale) return _mulDivQ192(priceX192, 1e18 * assetScale / quoteScale); } else { // invert: token0 per token1 uint256 inv = _divQ192(priceX192); return inv * assetScale / quoteScale; // inv already includes 1e18 } } // price = priceX192 / 2^192 * k, computed without overflow via 512-bit-free path: // split priceX192 into high/low through two shifts of 96. function _mulDivQ192(uint256 priceX192, uint256 k) internal pure returns (uint256) { // (priceX192 >> 96) * k >> 96 — loses at most 2^-96 relative precision, fine for WAD return ((priceX192 >> 96) * k) >> 96; } function _divQ192(uint256 priceX192) internal pure returns (uint256) { // 1e18 * 2^192 / priceX192, computed as (1e18 << 96) / (priceX192 >> 96) return (uint256(1e18) << 96) / (priceX192 >> 96); } /// @dev Canonical Uniswap v3 TickMath.getSqrtRatioAtTick (verbatim constants). function _getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { unchecked { uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); require(absTick <= 887272, "T"); uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; if (tick > 0) ratio = type(uint256).max / ratio; sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); } } }