210 lines
7.7 KiB
Solidity
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];
|
|
}
|
|
}
|