// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "../interfaces/IReserveAggregator.sol"; /** * @title ReserveAggregator * @dev Aggregates reserve attestations from multiple custodian types * Supports: Tier-1 banks (Chainlink PoR), crypto custodians (on-chain push), fund administrators (signed oracle) */ contract ReserveAggregator is IReserveAggregator, AccessControl, Pausable, ReentrancyGuard { bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); bytes32 public constant CUSTODIAN_MANAGER_ROLE = keccak256("CUSTODIAN_MANAGER_ROLE"); // Custodian storage mapping(bytes32 => CustodianInfo) public custodians; mapping(bytes32 => ReserveAttestation) public latestAttestations; bytes32[] public activeCustodianIds; // Reserve tracking uint256 public totalReserveValue; uint256 public lastUpdateTime; // Configuration uint256 public maxStalenessWindow = 24 hours; // Global max staleness uint256 public minimumCustodians = 1; // Minimum active custodians required modifier onlyValidCustodian(bytes32 custodianId) { require(custodians[custodianId].isActive, "Custodian not active"); _; } constructor(address admin) { require(admin != address(0), "Invalid admin address"); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(CUSTODIAN_MANAGER_ROLE, admin); _grantRole(ATTESTOR_ROLE, admin); } /** * @dev Add a new custodian to the aggregator * @param custodianId Unique identifier for custodian * @param name Human-readable custodian name * @param oracle Oracle address (Chainlink feed, contract, or EOA) * @param heartbeat Maximum staleness in seconds * @param custodianType 0=bank, 1=crypto, 2=fund_admin */ function addCustodian( bytes32 custodianId, string memory name, address oracle, uint256 heartbeat, uint8 custodianType ) external onlyRole(CUSTODIAN_MANAGER_ROLE) { require(custodianId != bytes32(0), "Invalid custodian ID"); require(oracle != address(0), "Invalid oracle address"); require(heartbeat > 0 && heartbeat <= maxStalenessWindow, "Invalid heartbeat"); require(custodianType <= 2, "Invalid custodian type"); require(!custodians[custodianId].isActive, "Custodian already exists"); custodians[custodianId] = CustodianInfo({ name: name, oracle: oracle, lastUpdateTime: 0, heartbeat: heartbeat, isActive: true, custodianType: custodianType }); activeCustodianIds.push(custodianId); emit CustodianAdded(custodianId, name, oracle); } /** * @dev Update custodian oracle and heartbeat */ function updateCustodian( bytes32 custodianId, address newOracle, uint256 newHeartbeat ) external onlyRole(CUSTODIAN_MANAGER_ROLE) onlyValidCustodian(custodianId) { require(newOracle != address(0), "Invalid oracle address"); require(newHeartbeat > 0 && newHeartbeat <= maxStalenessWindow, "Invalid heartbeat"); custodians[custodianId].oracle = newOracle; custodians[custodianId].heartbeat = newHeartbeat; emit CustodianUpdated(custodianId, newOracle, newHeartbeat); } /** * @dev Deactivate a custodian */ function deactivateCustodian(bytes32 custodianId) external onlyRole(CUSTODIAN_MANAGER_ROLE) { require(custodians[custodianId].isActive, "Custodian not active"); custodians[custodianId].isActive = false; // Remove from active array for (uint i = 0; i < activeCustodianIds.length; i++) { if (activeCustodianIds[i] == custodianId) { activeCustodianIds[i] = activeCustodianIds[activeCustodianIds.length - 1]; activeCustodianIds.pop(); break; } } emit CustodianDeactivated(custodianId); _updateTotalReserveValue(); } /** * @dev Attest reserves for a custodian (manual/signed attestation) * @param custodianId Custodian identifier * @param balance Reserve balance in token denomination * @param documentHash IPFS hash of supporting documentation */ function attestReserves( bytes32 custodianId, uint256 balance, bytes32 documentHash ) external onlyRole(ATTESTOR_ROLE) onlyValidCustodian(custodianId) nonReentrant { require(documentHash != bytes32(0), "Document hash required"); // For manual attestations, custodian type should be fund_admin (2) require(custodians[custodianId].custodianType == 2, "Manual attestation only for fund administrators"); latestAttestations[custodianId] = ReserveAttestation({ balance: balance, timestamp: block.timestamp, documentHash: documentHash, attestor: msg.sender, isValid: true }); custodians[custodianId].lastUpdateTime = block.timestamp; emit ReserveAttested(custodianId, balance, documentHash); _updateTotalReserveValue(); } /** * @dev Pull reserve value from Chainlink PoR feed (for bank custodians) * @param custodianId Custodian identifier */ function pullChainlinkReserves(bytes32 custodianId) external onlyValidCustodian(custodianId) { require(custodians[custodianId].custodianType == 0, "Only for bank custodians"); AggregatorV3Interface priceFeed = AggregatorV3Interface(custodians[custodianId].oracle); try priceFeed.latestRoundData() returns ( uint80 roundId, int256 price, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ) { require(price >= 0, "Invalid reserve value"); require(updatedAt > 0, "Invalid timestamp"); require(block.timestamp - updatedAt <= custodians[custodianId].heartbeat, "Data too stale"); latestAttestations[custodianId] = ReserveAttestation({ balance: uint256(price), timestamp: updatedAt, documentHash: bytes32(roundId), // Use round ID as reference attestor: custodians[custodianId].oracle, isValid: true }); custodians[custodianId].lastUpdateTime = updatedAt; emit ReserveAttested(custodianId, uint256(price), bytes32(roundId)); _updateTotalReserveValue(); } catch { revert("Failed to fetch Chainlink data"); } } /** * @dev Push reserve attestation from crypto custodian contract * @param custodianId Custodian identifier * @param balance Reserve balance * @param proof Cryptographic proof of reserves */ function pushCryptoReserves( bytes32 custodianId, uint256 balance, bytes calldata proof ) external onlyValidCustodian(custodianId) { require(custodians[custodianId].custodianType == 1, "Only for crypto custodians"); require(msg.sender == custodians[custodianId].oracle, "Only authorized oracle"); // Verify proof (implementation depends on custodian's proof system) require(_verifyReserveProof(balance, proof), "Invalid reserve proof"); latestAttestations[custodianId] = ReserveAttestation({ balance: balance, timestamp: block.timestamp, documentHash: keccak256(proof), attestor: msg.sender, isValid: true }); custodians[custodianId].lastUpdateTime = block.timestamp; emit ReserveAttested(custodianId, balance, keccak256(proof)); _updateTotalReserveValue(); } /** * @dev Get total reserve value across all active custodians */ function getTotalReserveValue() external view override returns (uint256, bool) { if (activeCustodianIds.length < minimumCustodians) { return (0, false); } uint256 total = 0; bool allValid = true; for (uint i = 0; i < activeCustodianIds.length; i++) { bytes32 custodianId = activeCustodianIds[i]; (uint256 value, bool isValid) = getCustodianReserveValue(custodianId); if (!isValid) { allValid = false; continue; } total += value; } return (total, allValid && total > 0); } /** * @dev Get reserve value from specific custodian */ function getCustodianReserveValue(bytes32 custodianId) public view override returns (uint256, bool) { if (!custodians[custodianId].isActive) { return (0, false); } ReserveAttestation memory attestation = latestAttestations[custodianId]; if (!attestation.isValid || attestation.timestamp == 0) { return (0, false); } // Check staleness uint256 staleness = block.timestamp - attestation.timestamp; if (staleness > custodians[custodianId].heartbeat) { return (0, false); } return (attestation.balance, true); } /** * @dev Get custodian information */ function getCustodianInfo(bytes32 custodianId) external view override returns (CustodianInfo memory) { return custodians[custodianId]; } /** * @dev Get latest attestation */ function getLatestAttestation(bytes32 custodianId) external view override returns (ReserveAttestation memory) { return latestAttestations[custodianId]; } /** * @dev Check for stale data across custodians */ function checkDataStaleness() external view override returns (bool, bytes32[] memory) { bytes32[] memory staleCustodians = new bytes32[](activeCustodianIds.length); uint256 staleCount = 0; for (uint i = 0; i < activeCustodianIds.length; i++) { bytes32 custodianId = activeCustodianIds[i]; ReserveAttestation memory attestation = latestAttestations[custodianId]; if (attestation.timestamp == 0) { staleCustodians[staleCount] = custodianId; staleCount++; continue; } uint256 staleness = block.timestamp - attestation.timestamp; if (staleness > custodians[custodianId].heartbeat) { staleCustodians[staleCount] = custodianId; staleCount++; } } // Resize array to actual stale count bytes32[] memory result = new bytes32[](staleCount); for (uint i = 0; i < staleCount; i++) { result[i] = staleCustodians[i]; } return (staleCount > 0, result); } /** * @dev Update configuration parameters */ function updateConfiguration( uint256 newMaxStaleness, uint256 newMinimumCustodians ) external onlyRole(DEFAULT_ADMIN_ROLE) { require(newMaxStaleness >= 1 hours, "Staleness too short"); require(newMaxStaleness <= 7 days, "Staleness too long"); require(newMinimumCustodians > 0, "Must have minimum custodians"); maxStalenessWindow = newMaxStaleness; minimumCustodians = newMinimumCustodians; } /** * @dev Internal function to verify reserve proofs (placeholder) */ function _verifyReserveProof(uint256 balance, bytes calldata proof) internal pure returns (bool) { // Implementation depends on custodian's specific proof system // Could be Merkle proof, ZK proof, signature verification, etc. return proof.length > 0 && balance > 0; } /** * @dev Internal function to recalculate total reserves */ function _updateTotalReserveValue() internal { (uint256 newTotal, bool isValid) = this.getTotalReserveValue(); if (isValid) { totalReserveValue = newTotal; lastUpdateTime = block.timestamp; } } /** * @dev Get active custodian count */ function getActiveCustodianCount() external view returns (uint256) { return activeCustodianIds.length; } /** * @dev Get all active custodian IDs */ function getActiveCustodianIds() external view returns (bytes32[] memory) { return activeCustodianIds; } /** * @dev Pause operations */ function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); } /** * @dev Unpause operations */ function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } }