// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {IPluginFeed, IAggregatorV3Like} from "./interfaces/IPluginFeed.sol"; import {PluginStalenessDefaults} from "./PluginStalenessDefaults.sol"; /// @title PluginOracleAdapter /// @notice Checked AggregatorV3-like read of a Plugin feed, in the feed's NATIVE decimals /// (16/18/25 on XDC; read `decimals()`). Always-on round checks: answer > 0, /// updatedAt != 0 and not in the future, answeredInRound >= roundId, age <= maxStaleness. /// Optional min/max answer bounds (0 = off). Access-controlled feeds that revert with /// "No access" surface as `FeedAccessDenied`; a revert with no error message surfaces as /// `FeedReverted(feed, "")`. Both are "reverting (access-controlled or broken)", the same /// category the feed explorer uses (see `PluginStalenessDefaults.knownReverting`). /// @dev Immutable, ownerless. Clean-room implementation. contract PluginOracleAdapter is IAggregatorV3Like { IPluginFeed public immutable feed; uint256 public immutable maxStaleness; // seconds uint8 public immutable feedDecimals; int256 public immutable minAnswer; // 0 = no lower bound int256 public immutable maxAnswer; // 0 = no upper bound bytes32 internal constant NO_ACCESS = keccak256("No access"); error ZeroAddress(); error BadConfig(); error UnknownStaleness(address feed); error FeedAccessDenied(address feed); error FeedReverted(address feed, bytes reason); error StalePrice(uint256 updatedAt, uint256 nowTs); error NonPositivePrice(int256 answer); error IncompleteRound(uint80 roundId, uint80 answeredInRound); error OutOfBounds(int256 answer); /// @param _maxStaleness seconds; 0 = use PluginStalenessDefaults (reverts if feed unknown) /// @param _minAnswer optional lower bound in native decimals (0 = off) /// @param _maxAnswer optional upper bound in native decimals (0 = off) constructor(IPluginFeed _feed, uint256 _maxStaleness, int256 _minAnswer, int256 _maxAnswer) { if (address(_feed) == address(0)) revert ZeroAddress(); if (_minAnswer < 0 || _maxAnswer < 0 || (_maxAnswer != 0 && _minAnswer > _maxAnswer)) revert BadConfig(); if (_maxStaleness == 0) { _maxStaleness = PluginStalenessDefaults.get(address(_feed)); if (_maxStaleness == 0) revert UnknownStaleness(address(_feed)); } feed = _feed; maxStaleness = _maxStaleness; minAnswer = _minAnswer; maxAnswer = _maxAnswer; try _feed.decimals() returns (uint8 d) { feedDecimals = d; } catch (bytes memory reason) { _rethrow(reason); } } function decimals() external view returns (uint8) { return feedDecimals; } function description() external view returns (string memory) { return feed.description(); } function version() external pure returns (uint256) { return 2; } /// @notice Checked latest round in native decimals. Reverts on any failed check. function latestRoundData() public view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { try feed.latestRoundData() returns (uint80 r, int256 a, uint256 s, uint256 u, uint80 air) { (roundId, answer, startedAt, updatedAt, answeredInRound) = (r, a, s, u, air); } catch (bytes memory reason) { _rethrow(reason); } if (answer <= 0) revert NonPositivePrice(answer); if (updatedAt == 0 || updatedAt > block.timestamp || block.timestamp - updatedAt > maxStaleness) { revert StalePrice(updatedAt, block.timestamp); } if (answeredInRound < roundId) revert IncompleteRound(roundId, answeredInRound); if ((minAnswer != 0 && answer < minAnswer) || (maxAnswer != 0 && answer > maxAnswer)) { revert OutOfBounds(answer); } } /// @notice Convenience: checked price as uint256 in native decimals. function latestPrice() external view returns (uint256) { (, int256 a,,,) = latestRoundData(); return uint256(a); } /// @dev Map Error("No access") to FeedAccessDenied; wrap anything else. function _rethrow(bytes memory reason) internal view { if (reason.length >= 68 && bytes4(reason) == 0x08c379a0) { bytes memory payload = new bytes(reason.length - 4); for (uint256 i; i < payload.length; ++i) payload[i] = reason[i + 4]; if (keccak256(bytes(abi.decode(payload, (string)))) == NO_ACCESS) revert FeedAccessDenied(address(feed)); } revert FeedReverted(address(feed), reason); } }