// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PluginOracleAdapter} from "./PluginOracleAdapter.sol"; import {Owned} from "./utils/Owned.sol"; /// @notice Cumulative-price source. `cumulative` is in source-specific raw units and may wrap; /// `quote` converts an average raw value to a price in the primary's decimals and /// enforces the source's own safety checks (e.g. minimum liquidity). interface ITwapSource { function priceCumulative() external view returns (uint256 cumulative, uint256 timestamp); function quote(uint256 avgRaw) external view returns (uint256 price); } /// @title TwapFallback /// @notice Returns the primary (Plugin adapter) price; falls back to a TWAP when the /// primary reverts (stale/invalid/no access) or is outside [minPrice, maxPrice]. /// TWAP window must be within [minWindow, maxWindow]. /// @dev The fallback is OFF by default (`fallbackEnabled = false`): the owner must enable it /// deliberately once a deep enough pool exists. On XDC mainnet the XSwap V2 WXDC/USDC pair /// held only ~42 USDC on 2026-10-21, so it must not be used (its min-liquidity check fails). /// `checkpoint()` stays callable by anyone by design: it can only move the TWAP start forward /// after `minWindow`, the read is bounded by [minWindow, maxWindow], and no value is held here. contract TwapFallback is Owned { PluginOracleAdapter public immutable primary; ITwapSource public immutable twap; uint256 public immutable minPrice; uint256 public immutable maxPrice; uint256 public immutable minWindow; uint256 public immutable maxWindow; uint256 public cpCumulative; uint256 public cpTimestamp; error WindowTooShort(uint256 elapsed); error WindowTooLong(uint256 elapsed); error NoValidPrice(); error BadConfig(); error FallbackDisabled(); bool public fallbackEnabled; // false by default event FallbackEnabled(bool enabled); constructor( PluginOracleAdapter _primary, ITwapSource _twap, uint256 _min, uint256 _max, uint256 _minWindow, uint256 _maxWindow, address _owner ) Owned(_owner) { if (_min == 0 || _min > _max || _minWindow == 0 || _maxWindow < _minWindow) revert BadConfig(); primary = _primary; twap = _twap; minPrice = _min; maxPrice = _max; minWindow = _minWindow; maxWindow = _maxWindow; (cpCumulative, cpTimestamp) = _twap.priceCumulative(); } function setFallbackEnabled(bool on) external onlyOwner { fallbackEnabled = on; emit FallbackEnabled(on); } /// @notice Permissionless on purpose (see contract notes). function checkpoint() external { (uint256 c, uint256 t) = twap.priceCumulative(); if (t - cpTimestamp < minWindow) revert WindowTooShort(t - cpTimestamp); (cpCumulative, cpTimestamp) = (c, t); } function twapPrice() public view returns (uint256) { (uint256 c, uint256 t) = twap.priceCumulative(); uint256 elapsed = t - cpTimestamp; if (elapsed < minWindow) revert WindowTooShort(elapsed); if (elapsed > maxWindow) revert WindowTooLong(elapsed); uint256 delta; unchecked { delta = c - cpCumulative; // accumulators may wrap by design } return twap.quote(delta / elapsed); } /// @return price in primary decimals; usedFallback true if the TWAP was used function price() external view returns (uint256, bool usedFallback) { try primary.latestPrice() returns (uint256 p) { if (p >= minPrice && p <= maxPrice) return (p, false); } catch {} if (!fallbackEnabled) revert FallbackDisabled(); uint256 tp = twapPrice(); if (tp < minPrice || tp > maxPrice) revert NoValidPrice(); return (tp, true); } }