// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title AereNavOracleAdapter — owner-attested per-asset RWA NAV price feed * @notice Fixes audit finding F12. AereLendingOracle's FeedType.NavOracle path * calls `IAereNavOracleAdapter(source).latestForAsset(bytes32)` and * expects back a per-asset (nav, timestamp) pair it can normalise to an * 18-decimal collateral price. The deployed AereNavOracle * (0xC8D12E44f10b03477330b35115b432750831fEBD) does NOT and CANNOT * satisfy that interface: * * - It stores Merkle ROOTS of per-epoch reserve snapshots, not * per-asset values. Given only a bytes32 asset key you cannot * recover a value from a root; you would need the full leaf * preimage (asset, chainId, reserveAmount, decimals) PLUS a Merkle * proof, and even then `verifyReserve` only returns a boolean. * - Its leaf field is a total RESERVE AMOUNT (proof-of-backing, e.g. * "5,000,000 USDC locked on mainnet"), which is a quantity, not a * per-unit price. It does not map to a lending collateral price. * * So a passthrough adapter that "reads" the NavOracle is impossible. * The honest fix (audit option (a)) is a PURPOSE-BUILT RWA-NAV price * adapter with an OWNER-SET per-asset value feed — exactly the * "Foundation-attested NAV pulled ... for RWAs that don't have a live * market price" that AereLendingOracle's own NatSpec describes. * * DESIGN / SAFETY: * - `setNav` is owner-only (Foundation). The adapter is the upstream * FEED SOURCE for a NavOracle-type feed. The AereLendingOracle * layered on top still enforces its own staleness window and * deviation cap on every read, so a single attested value cannot * jump the lending price beyond `maxDeviationBps`. * - `latestForAsset` returns (0,0) for an un-attested asset, which * AereLendingOracle already treats as InvalidFeedResponse. It never * returns a future-dated timestamp. * - NAV values are stored in the adapter's native decimals; the * consuming feed's `sourceDecimals` handles normalisation. The * Foundation convention is 18-decimal NAVs (sourceDecimals = 18). * * PROVENANCE: * - `NAV_ORACLE` records the associated AereNavOracle whose public * Merkle reserve attestations back these assets. It is stored for * transparency ONLY and is NEVER read to derive a price (see above). */ contract AereNavOracleAdapter is Ownable { /// @notice The associated reserve-attestation oracle (AereNavOracle). /// Recorded for provenance; NOT read to derive prices (its data is /// Merkle-rooted reserve totals, not per-asset prices). address public immutable NAV_ORACLE; struct Reading { uint256 nav; // per-asset value in the adapter's native decimals (18 by convention) uint64 timestamp; // block.timestamp at attestation bool set; } /// @notice assetKey -> latest attested reading. mapping(bytes32 => Reading) public readings; /* --------------------------------- events ------------------------------- */ event NavSet(bytes32 indexed assetKey, uint256 nav, uint64 timestamp); /* --------------------------------- errors ------------------------------- */ error ZeroNav(); /* ------------------------------- constructor ---------------------------- */ /// @param navOracle The associated AereNavOracle (provenance only; may be /// address(0) if none). Not read for prices. constructor(address navOracle) { NAV_ORACLE = navOracle; } /* --------------------------------- admin -------------------------------- */ /// @notice Foundation attests the current per-asset NAV for `assetKey`. /// Stamps `block.timestamp` so downstream staleness checks work. function setNav(bytes32 assetKey, uint256 nav) external onlyOwner { if (nav == 0) revert ZeroNav(); readings[assetKey] = Reading({ nav: nav, timestamp: uint64(block.timestamp), set: true }); emit NavSet(assetKey, nav, uint64(block.timestamp)); } /* --------------------------------- view --------------------------------- */ /// @notice Matches AereLendingOracle's IAereNavOracleAdapter. Returns the /// latest attested (nav, timestamp) for `assetKey`, or (0,0) if the /// asset has never been attested (LendingOracle treats that as /// InvalidFeedResponse). function latestForAsset(bytes32 assetKey) external view returns (uint256 nav, uint64 timestamp) { Reading storage r = readings[assetKey]; return (r.nav, r.timestamp); } }