// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "../interfaces/IReserveOracle.sol"; /** * @title ReserveOracle * @notice Quorum-based oracle system for verifying fiat reserves * @dev Requires quorum of oracle reports before accepting reserve values */ contract ReserveOracle is IReserveOracle, AccessControl, ReentrancyGuard { bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); uint256 public quorumThreshold; // Number of reports required for quorum (default: 3) uint256 public stalenessThreshold; // Maximum age of reports in seconds (default: 3600 = 1 hour) // Currency code => ReserveReport[] mapping(string => ReserveReport[]) private _reports; // Currency code => verified reserve (consensus value) mapping(string => uint256) private _verifiedReserves; mapping(string => uint256) private _lastUpdate; // Track valid oracles mapping(address => bool) public isOracle; constructor(address admin, uint256 quorumThreshold_, uint256 stalenessThreshold_) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(ORACLE_ROLE, admin); quorumThreshold = quorumThreshold_; stalenessThreshold = stalenessThreshold_; isOracle[admin] = true; } /** * @notice Submit reserve report for a currency * @param currencyCode ISO-4217 currency code * @param reserveBalance Reserve balance in base currency units * @param attestationHash Hash of custodian attestation */ function submitReserveReport( string memory currencyCode, uint256 reserveBalance, bytes32 attestationHash ) external override onlyRole(ORACLE_ROLE) nonReentrant { require(isOracle[msg.sender], "ReserveOracle: not authorized oracle"); require(reserveBalance > 0, "ReserveOracle: zero reserve"); // Validate ISO-4217 format (basic check) bytes memory codeBytes = bytes(currencyCode); require(codeBytes.length == 3, "ReserveOracle: invalid currency code format"); // Remove stale reports _removeStaleReports(currencyCode); // Add new report _reports[currencyCode].push(ReserveReport({ reporter: msg.sender, reserveBalance: reserveBalance, timestamp: block.timestamp, attestationHash: attestationHash, isValid: true })); emit ReserveReportSubmitted(currencyCode, msg.sender, reserveBalance, block.timestamp); // Check if quorum is met and update verified reserve (bool quorumMet, ) = this.isQuorumMet(currencyCode); if (quorumMet) { uint256 consensusReserve = this.getConsensusReserve(currencyCode); _verifiedReserves[currencyCode] = consensusReserve; _lastUpdate[currencyCode] = block.timestamp; emit QuorumMet(currencyCode, consensusReserve); } } /** * @notice Get verified reserve balance for a currency * @param currencyCode ISO-4217 currency code * @return reserveBalance Verified reserve balance * @return timestamp Last update timestamp */ function getVerifiedReserve(string memory currencyCode) external view override returns ( uint256 reserveBalance, uint256 timestamp ) { return (_verifiedReserves[currencyCode], _lastUpdate[currencyCode]); } /** * @notice Check if oracle quorum is met for a currency * @param currencyCode ISO-4217 currency code * @return quorumMet True if quorum is met * @return reportCount Number of valid reports */ function isQuorumMet(string memory currencyCode) external view override returns (bool quorumMet, uint256 reportCount) { ReserveReport[] memory reports = _reports[currencyCode]; uint256 validCount = 0; uint256 cutoffTime = block.timestamp > stalenessThreshold ? block.timestamp - stalenessThreshold : 0; for (uint256 i = 0; i < reports.length; i++) { if (reports[i].isValid && reports[i].timestamp >= cutoffTime) { validCount++; } } reportCount = validCount; quorumMet = validCount >= quorumThreshold; } /** * @notice Get consensus reserve balance (median/average of quorum reports) * @param currencyCode ISO-4217 currency code * @return consensusReserve Consensus reserve balance */ function getConsensusReserve(string memory currencyCode) external view override returns (uint256 consensusReserve) { ReserveReport[] memory reports = _reports[currencyCode]; // Remove stale and invalid reports uint256[] memory validReserves = new uint256[](reports.length); uint256 validCount = 0; uint256 cutoffTime = block.timestamp > stalenessThreshold ? block.timestamp - stalenessThreshold : 0; for (uint256 i = 0; i < reports.length; i++) { if (reports[i].isValid && reports[i].timestamp >= cutoffTime) { validReserves[validCount] = reports[i].reserveBalance; validCount++; } } if (validCount == 0) { return 0; } // Sort and calculate median (simple average if count is even) // For simplicity, calculate average (in production, use median for robustness) uint256 sum = 0; for (uint256 i = 0; i < validCount; i++) { sum += validReserves[i]; } consensusReserve = sum / validCount; } /** * @notice Remove stale reports for a currency * @param currencyCode ISO-4217 currency code */ function _removeStaleReports(string memory currencyCode) internal { ReserveReport[] storage reports = _reports[currencyCode]; uint256 cutoffTime = block.timestamp > stalenessThreshold ? block.timestamp - stalenessThreshold : 0; // Mark stale reports as invalid for (uint256 i = 0; i < reports.length; i++) { if (reports[i].timestamp < cutoffTime) { reports[i].isValid = false; } } } /** * @notice Add oracle * @param oracle Oracle address */ function addOracle(address oracle) external onlyRole(DEFAULT_ADMIN_ROLE) { require(oracle != address(0), "ReserveOracle: zero address"); isOracle[oracle] = true; _grantRole(ORACLE_ROLE, oracle); } /** * @notice Remove oracle * @param oracle Oracle address */ function removeOracle(address oracle) external onlyRole(DEFAULT_ADMIN_ROLE) { require(isOracle[oracle], "ReserveOracle: not an oracle"); isOracle[oracle] = false; _revokeRole(ORACLE_ROLE, oracle); } /** * @notice Set quorum threshold * @param threshold New quorum threshold */ function setQuorumThreshold(uint256 threshold) external onlyRole(DEFAULT_ADMIN_ROLE) { require(threshold > 0, "ReserveOracle: zero threshold"); quorumThreshold = threshold; } /** * @notice Set staleness threshold * @param threshold New staleness threshold in seconds */ function setStalenessThreshold(uint256 threshold) external onlyRole(DEFAULT_ADMIN_ROLE) { stalenessThreshold = threshold; } /** * @notice Get reports for a currency * @param currencyCode ISO-4217 currency code * @return reports Array of reserve reports */ function getReports(string memory currencyCode) external view returns (ReserveReport[] memory reports) { return _reports[currencyCode]; } }