Files
smom-dbis-138/contracts/iso4217w/oracle/ReserveOracle.sol
2026-03-02 12:14:09 -08:00

210 lines
7.7 KiB
Solidity

// 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];
}
}