- Complete MeridianToken standard with compliance & reserve checks - Multi-custodian ReserveAggregator supporting bank/crypto/fund admin - Comprehensive Compliance engine with KYC/AML/sanctions - Full interface definitions and deployment scripts - Test suite for core functionality - Ready for GBP launch with Anchorage custody integration
375 lines
13 KiB
Solidity
375 lines
13 KiB
Solidity
// 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();
|
|
}
|
|
}
|