// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ITwapSource} from "./TwapFallback.sol"; /// @notice Minimal UniswapV2-style pair surface (XSwap V2 on XDC). interface IV2Pair { function token0() external view returns (address); function token1() external view returns (address); function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); function price0CumulativeLast() external view returns (uint256); function price1CumulativeLast() external view returns (uint256); } interface IERC20Decimals { function decimals() external view returns (uint8); } /// @title XSwapV2TwapSource /// @notice ITwapSource over an XSwap V2 pair: price of `base` in `quote` token, using the pair's /// UQ112x112 cumulative price extended to the current block. `quote()` reverts unless the /// pair holds at least `minQuoteReserve` of the quote token (raw units). /// @dev Clean-room implementation of the well-known V2 accumulator formula. contract XSwapV2TwapSource is ITwapSource { IV2Pair public immutable pair; bool public immutable baseIsToken0; uint256 public immutable minQuoteReserve; uint256 internal immutable num; // 10**(outDecimals + baseDecimals) uint256 internal immutable den; // 10**quoteDecimals error NotInPair(); error BadConfig(); error InsufficientLiquidity(uint256 quoteReserve, uint256 minQuoteReserve); constructor(IV2Pair _pair, address base, uint8 outDecimals, uint256 _minQuoteReserve) { address t0 = _pair.token0(); address t1 = _pair.token1(); if (base != t0 && base != t1) revert NotInPair(); if (_minQuoteReserve == 0) revert BadConfig(); pair = _pair; baseIsToken0 = base == t0; uint8 bd = IERC20Decimals(base).decimals(); uint8 qd = IERC20Decimals(base == t0 ? t1 : t0).decimals(); if (outDecimals > 18 || bd > 18 || qd > 18) revert BadConfig(); num = 10 ** (uint256(outDecimals) + bd); den = 10 ** uint256(qd); minQuoteReserve = _minQuoteReserve; } function priceCumulative() external view returns (uint256 c, uint256 timestamp) { (uint112 r0, uint112 r1, uint32 tsLast) = pair.getReserves(); c = baseIsToken0 ? pair.price0CumulativeLast() : pair.price1CumulativeLast(); uint32 nowTs = uint32(block.timestamp); if (tsLast != nowTs && r0 != 0 && r1 != 0) { unchecked { uint256 elapsed = uint32(nowTs - tsLast); // wraps like the pair's own clock uint256 px = baseIsToken0 ? (uint256(r1) << 112) / r0 : (uint256(r0) << 112) / r1; c += px * elapsed; } } timestamp = block.timestamp; } /// @param avgRaw average UQ112x112 price (quote raw units per base raw unit) function quote(uint256 avgRaw) external view returns (uint256) { (uint112 r0, uint112 r1,) = pair.getReserves(); uint256 q = baseIsToken0 ? r1 : r0; if (q < minQuoteReserve) revert InsufficientLiquidity(q, minQuoteReserve); uint256 hi = avgRaw >> 112; uint256 lo = avgRaw & ((uint256(1) << 112) - 1); return hi * num / den + (lo * num) / (den << 112); } }