// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "../oracle/IAggregator.sol"; import "./IReserveSystem.sol"; /** * @title OraclePriceFeed * @notice Integrates Reserve System with Chainlink-compatible oracle aggregators * @dev Provides price feeds from multiple sources with aggregation and validation */ contract OraclePriceFeed is AccessControl { bytes32 public constant PRICE_FEED_UPDATER_ROLE = keccak256("PRICE_FEED_UPDATER_ROLE"); IReserveSystem public reserveSystem; // Asset to aggregator mapping mapping(address => address) public aggregators; // Asset to price multiplier (for decimals conversion) mapping(address => uint256) public priceMultipliers; // Price feed update interval (seconds) uint256 public updateInterval = 30; // Last update timestamp per asset mapping(address => uint256) public lastUpdateTime; event AggregatorSet(address indexed asset, address indexed aggregator, uint256 multiplier); event PriceFeedUpdated(address indexed asset, uint256 price, uint256 timestamp); constructor(address admin, address reserveSystem_) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(PRICE_FEED_UPDATER_ROLE, admin); reserveSystem = IReserveSystem(reserveSystem_); } /** * @notice Set aggregator for an asset * @param asset Address of the asset * @param aggregator Address of the Chainlink aggregator * @param multiplier Price multiplier for decimals conversion (e.g., 1e10 for 8-decimal aggregator to 18 decimals) */ function setAggregator( address asset, address aggregator, uint256 multiplier ) external onlyRole(DEFAULT_ADMIN_ROLE) { require(asset != address(0), "OraclePriceFeed: zero asset"); require(aggregator != address(0), "OraclePriceFeed: zero aggregator"); require(multiplier > 0, "OraclePriceFeed: zero multiplier"); aggregators[asset] = aggregator; priceMultipliers[asset] = multiplier; emit AggregatorSet(asset, aggregator, multiplier); } /** * @notice Update price feed from aggregator * @param asset Address of the asset */ function updatePriceFeed(address asset) public onlyRole(PRICE_FEED_UPDATER_ROLE) { address aggregator = aggregators[asset]; require(aggregator != address(0), "OraclePriceFeed: aggregator not set"); IAggregator agg = IAggregator(aggregator); (, int256 answer, , uint256 updatedAt, ) = agg.latestRoundData(); require(answer > 0, "OraclePriceFeed: invalid price"); require(updatedAt > 0, "OraclePriceFeed: invalid timestamp"); require(block.timestamp - updatedAt <= updateInterval * 2, "OraclePriceFeed: stale price"); uint256 price = uint256(answer) * priceMultipliers[asset]; reserveSystem.updatePriceFeed(asset, price, updatedAt); lastUpdateTime[asset] = block.timestamp; emit PriceFeedUpdated(asset, price, updatedAt); } /** * @notice Update multiple price feeds * @param assets Array of asset addresses */ function updateMultiplePriceFeeds(address[] calldata assets) external onlyRole(PRICE_FEED_UPDATER_ROLE) { for (uint256 i = 0; i < assets.length; i++) { updatePriceFeed(assets[i]); } } /** * @notice Get current price from aggregator and update reserve system * @param asset Address of the asset * @return price Current price * @return timestamp Price timestamp */ function getAndUpdatePrice(address asset) external returns (uint256 price, uint256 timestamp) { updatePriceFeed(asset); return reserveSystem.getPrice(asset); } /** * @notice Set update interval * @param interval New update interval in seconds */ function setUpdateInterval(uint256 interval) external onlyRole(DEFAULT_ADMIN_ROLE) { require(interval > 0, "OraclePriceFeed: zero interval"); updateInterval = interval; } /** * @notice Check if price feed needs update * @param asset Address of the asset * @return updateNeeded True if update is needed */ function needsUpdate(address asset) external view returns (bool updateNeeded) { uint256 lastUpdate = lastUpdateTime[asset]; if (lastUpdate == 0) { return true; } return block.timestamp - lastUpdate >= updateInterval; } }