// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import "./AerePyth.sol"; /// Existing AereOracle interface (median multi-reporter feed, deployed 2026-05-07). interface IAereOracleLegacy { function getPrice(bytes32 symbol) external view returns (uint128 price, uint64 timestamp, uint256 contributors); } /** * @title AereOracleAdapter * @notice Unified price-query interface for AERE Network. * * Bridges between: * - Legacy AereOracle (single multi-reporter median feed for BTC/USD + ETH/USD) * - AerePyth (Pyth-compatible pull oracle; 700+ feeds when public Pyth network onboards us) * * Routing logic (per symbol): * - If a Pyth feed ID is registered for the symbol, prefer Pyth (newer, more * trustworthy, signed by publisher set). * - Else fall back to legacy AereOracle. * - If neither has the symbol, revert. * * This adapter lets every AERE dApp keep using a single oracle address and one * `getPrice(symbol)` signature, while the underlying data source migrates * from AereOracle → AerePyth → canonical Pyth as the network matures. * * Symbol convention (bytes32): * keccak256("BTC/USD"), keccak256("ETH/USD"), keccak256("AERE/USD"), etc. */ contract AereOracleAdapter is Ownable { IAereOracleLegacy public legacy; AerePyth public pyth; /// symbol (keccak256) → Pyth price feed ID. If unset, fall back to legacy. mapping(bytes32 => bytes32) public pythFeedId; /// Maximum age (seconds) accepted for legacy oracle reads. uint256 public legacyMaxAge = 600; event PythRegistered(bytes32 indexed symbol, bytes32 indexed pythFeedId); event SourceUpdated(string source, address indexed addr); constructor(IAereOracleLegacy _legacy, AerePyth _pyth) { legacy = _legacy; pyth = _pyth; } // ───────────────────── Admin ───────────────────── function setLegacy(IAereOracleLegacy _legacy) external onlyOwner { legacy = _legacy; emit SourceUpdated("legacy", address(_legacy)); } function setPyth(AerePyth _pyth) external onlyOwner { pyth = _pyth; emit SourceUpdated("pyth", address(_pyth)); } function registerPythFeed(bytes32 symbol, bytes32 feedId) external onlyOwner { pythFeedId[symbol] = feedId; emit PythRegistered(symbol, feedId); } function setLegacyMaxAge(uint256 _age) external onlyOwner { legacyMaxAge = _age; } // ───────────────────── Unified read ───────────────────── /** * @notice Universal price read. * @return price1e8 Price scaled to 1e8 (same as AereOracle's format). * @return updatedAt unix seconds. * @return source "pyth" or "legacy". */ function quote(bytes32 symbol) external view returns (uint256 price1e8, uint256 updatedAt, string memory source) { bytes32 feedId = pythFeedId[symbol]; if (feedId != bytes32(0)) { PythPrice memory p = pyth.getPriceUnsafe(feedId); require(p.publishTime > 0, "Adapter: no pyth data"); // Pyth stores price + exponent; normalise to 1e8. // expo is negative for fractional prices (e.g. -8 for $30000.12345678). uint256 abs; if (p.price < 0) { // Negative price means market closed or invalid; revert. revert("Adapter: negative price"); } abs = uint256(uint64(p.price)); int32 expo = p.expo; if (expo == -8) { price1e8 = abs; } else if (expo > -8) { // expo is e.g. -6 (USDC), -2 (some equities). Scale up. price1e8 = abs * (10 ** uint256(int256(-8 - expo))); } else { // expo is e.g. -12, -18 (some assets). Scale down. price1e8 = abs / (10 ** uint256(int256(expo + 8))); } updatedAt = p.publishTime; source = "pyth"; return (price1e8, updatedAt, source); } // Fall back to legacy AereOracle. (uint128 lp, uint64 lts, ) = legacy.getPrice(symbol); require(lp > 0, "Adapter: no legacy data"); require(block.timestamp - uint256(lts) <= legacyMaxAge, "Adapter: legacy stale"); price1e8 = uint256(lp); updatedAt = uint256(lts); source = "legacy"; } /// Convenience: just return the price1e8. function priceOf(bytes32 symbol) external view returns (uint256 price1e8) { (price1e8, , ) = this.quote(symbol); } }