Initial Meridian Protocol implementation

- 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
This commit is contained in:
Claude AI
2026-04-16 19:42:26 +00:00
commit 7f001ff5f0
14 changed files with 9906 additions and 0 deletions

View File

@@ -0,0 +1,374 @@
// 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();
}
}

View File

@@ -0,0 +1,379 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "../interfaces/ICompliance.sol";
/**
* @title Compliance
* @dev Handles KYC whitelisting, sanctions screening, and AML velocity limits
*/
contract Compliance is ICompliance, AccessControl, Pausable {
bytes32 public constant KYC_MANAGER_ROLE = keccak256("KYC_MANAGER_ROLE");
bytes32 public constant SANCTIONS_MANAGER_ROLE = keccak256("SANCTIONS_MANAGER_ROLE");
bytes32 public constant AML_MANAGER_ROLE = keccak256("AML_MANAGER_ROLE");
// KYC storage
mapping(address => KYCRecord) public kycRecords;
mapping(address => bool) public sanctionedAddresses;
mapping(address => VelocityLimits) public velocityLimits;
// Global settings
uint256 public defaultDailyLimit = 10000 * 10**18; // 10,000 tokens default
uint256 public defaultMonthlyLimit = 100000 * 10**18; // 100,000 tokens default
uint256 public ctrThreshold = 10000 * 10**18; // Currency Transaction Report threshold
uint256 public sarThreshold = 5000 * 10**18; // Suspicious Activity Report threshold
uint256 public sanctionsListHash; // Hash of current sanctions list
// KYC levels and their associated limits
mapping(uint256 => uint256) public kycLevelDailyLimits;
mapping(uint256 => uint256) public kycLevelMonthlyLimits;
constructor(address admin) {
require(admin != address(0), "Invalid admin address");
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(KYC_MANAGER_ROLE, admin);
_grantRole(SANCTIONS_MANAGER_ROLE, admin);
_grantRole(AML_MANAGER_ROLE, admin);
// Set default KYC level limits
kycLevelDailyLimits[1] = 1000 * 10**18; // Basic: 1,000
kycLevelDailyLimits[2] = 10000 * 10**18; // Enhanced: 10,000
kycLevelDailyLimits[3] = 100000 * 10**18; // Institutional: 100,000
kycLevelMonthlyLimits[1] = 10000 * 10**18; // Basic: 10,000
kycLevelMonthlyLimits[2] = 100000 * 10**18; // Enhanced: 100,000
kycLevelMonthlyLimits[3] = 1000000 * 10**18; // Institutional: 1,000,000
}
/**
* @dev Add address to KYC whitelist
* @param account Address to whitelist
* @param kycLevel KYC level (1=basic, 2=enhanced, 3=institutional)
* @param jurisdiction KYC jurisdiction (e.g., "US", "UK", "EU")
* @param expiryTime When KYC expires (0 for no expiry)
*/
function whitelistAddress(
address account,
uint256 kycLevel,
string memory jurisdiction,
uint256 expiryTime
) external onlyRole(KYC_MANAGER_ROLE) {
require(account != address(0), "Invalid address");
require(kycLevel >= 1 && kycLevel <= 3, "Invalid KYC level");
require(expiryTime == 0 || expiryTime > block.timestamp, "Invalid expiry time");
kycRecords[account] = KYCRecord({
isWhitelisted: true,
kycLevel: kycLevel,
whitelistTime: block.timestamp,
expiryTime: expiryTime,
jurisdiction: jurisdiction
});
// Set velocity limits based on KYC level
_setVelocityLimits(
account,
kycLevelDailyLimits[kycLevel],
kycLevelMonthlyLimits[kycLevel]
);
emit AccountWhitelisted(account, kycLevel, jurisdiction);
}
/**
* @dev Remove address from KYC whitelist
* @param account Address to remove
* @param reason Reason for removal
*/
function blacklistAddress(
address account,
string memory reason
) external onlyRole(KYC_MANAGER_ROLE) {
require(account != address(0), "Invalid address");
kycRecords[account].isWhitelisted = false;
emit AccountBlacklisted(account, reason);
}
/**
* @dev Add address to sanctions list
* @param account Address to sanction
*/
function addToSanctionsList(address account) external onlyRole(SANCTIONS_MANAGER_ROLE) {
require(account != address(0), "Invalid address");
sanctionedAddresses[account] = true;
}
/**
* @dev Remove address from sanctions list
* @param account Address to remove from sanctions
*/
function removeFromSanctionsList(address account) external onlyRole(SANCTIONS_MANAGER_ROLE) {
sanctionedAddresses[account] = false;
}
/**
* @dev Batch update sanctions list
* @param accounts Array of addresses to update
* @param sanctioned Array of sanctions status (true/false)
* @param newListHash Hash of the new sanctions list for verification
*/
function batchUpdateSanctions(
address[] memory accounts,
bool[] memory sanctioned,
uint256 newListHash
) external onlyRole(SANCTIONS_MANAGER_ROLE) {
require(accounts.length == sanctioned.length, "Array length mismatch");
require(newListHash != 0, "Invalid list hash");
for (uint256 i = 0; i < accounts.length; i++) {
sanctionedAddresses[accounts[i]] = sanctioned[i];
}
sanctionsListHash = newListHash;
emit SanctionsListUpdated(newListHash);
}
/**
* @dev Set custom velocity limits for address
* @param account Address to set limits for
* @param dailyLimit Daily transaction limit
* @param monthlyLimit Monthly transaction limit
*/
function setVelocityLimits(
address account,
uint256 dailyLimit,
uint256 monthlyLimit
) external onlyRole(AML_MANAGER_ROLE) {
_setVelocityLimits(account, dailyLimit, monthlyLimit);
}
/**
* @dev Internal function to set velocity limits
*/
function _setVelocityLimits(
address account,
uint256 dailyLimit,
uint256 monthlyLimit
) internal {
require(account != address(0), "Invalid address");
require(dailyLimit > 0 && monthlyLimit > 0, "Limits must be positive");
require(monthlyLimit >= dailyLimit, "Monthly limit must be >= daily limit");
VelocityLimits storage limits = velocityLimits[account];
uint256 today = block.timestamp / 1 days;
uint256 thisMonth = block.timestamp / 30 days;
// Reset counters if this is first time setting limits
if (limits.dailyLimit == 0) {
limits.dailySpent = 0;
limits.monthlySpent = 0;
limits.lastDayReset = today;
limits.lastMonthReset = thisMonth;
}
limits.dailyLimit = dailyLimit;
limits.monthlyLimit = monthlyLimit;
emit VelocityLimitsUpdated(account, dailyLimit, monthlyLimit);
}
/**
* @dev Check if address is whitelisted
*/
function isWhitelisted(address account) external view override returns (bool) {
KYCRecord memory record = kycRecords[account];
if (!record.isWhitelisted) {
return false;
}
// Check expiry
if (record.expiryTime != 0 && record.expiryTime <= block.timestamp) {
return false;
}
return true;
}
/**
* @dev Check if address is sanctioned
*/
function isSanctioned(address account) external view override returns (bool) {
return sanctionedAddresses[account];
}
/**
* @dev Check velocity limits for transaction
*/
function checkVelocityLimits(address account, uint256 amount) external view override returns (bool) {
VelocityLimits memory limits = velocityLimits[account];
// If no limits set, use defaults based on KYC level
if (limits.dailyLimit == 0) {
KYCRecord memory kycRecord = kycRecords[account];
if (kycRecord.kycLevel == 0) {
return amount <= defaultDailyLimit;
}
return amount <= kycLevelDailyLimits[kycRecord.kycLevel];
}
uint256 today = block.timestamp / 1 days;
uint256 thisMonth = block.timestamp / 30 days;
// Reset daily counter if needed
uint256 dailySpent = limits.dailySpent;
if (today > limits.lastDayReset) {
dailySpent = 0;
}
// Reset monthly counter if needed
uint256 monthlySpent = limits.monthlySpent;
if (thisMonth > limits.lastMonthReset) {
monthlySpent = 0;
}
// Check both daily and monthly limits
return (dailySpent + amount <= limits.dailyLimit) &&
(monthlySpent + amount <= limits.monthlyLimit);
}
/**
* @dev Update velocity tracking after transaction
*/
function updateVelocityTracking(address account, uint256 amount) external override {
// Only allow calls from authorized token contracts
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Unauthorized velocity update");
VelocityLimits storage limits = velocityLimits[account];
// Initialize if needed
if (limits.dailyLimit == 0) {
KYCRecord memory kycRecord = kycRecords[account];
if (kycRecord.kycLevel > 0) {
_setVelocityLimits(
account,
kycLevelDailyLimits[kycRecord.kycLevel],
kycLevelMonthlyLimits[kycRecord.kycLevel]
);
} else {
_setVelocityLimits(account, defaultDailyLimit, defaultMonthlyLimit);
}
}
uint256 today = block.timestamp / 1 days;
uint256 thisMonth = block.timestamp / 30 days;
// Reset daily counter if new day
if (today > limits.lastDayReset) {
limits.dailySpent = 0;
limits.lastDayReset = today;
}
// Reset monthly counter if new month
if (thisMonth > limits.lastMonthReset) {
limits.monthlySpent = 0;
limits.lastMonthReset = thisMonth;
}
// Update spending
limits.dailySpent += amount;
limits.monthlySpent += amount;
}
/**
* @dev Check if transaction should trigger AML reporting
*/
function checkReportingThresholds(
address from,
address to,
uint256 amount
) external view override returns (bool shouldReport, uint8 reportType) {
// CTR - Currency Transaction Report (large transactions)
if (amount >= ctrThreshold) {
return (true, 1);
}
// SAR - Suspicious Activity Report (pattern analysis would go here)
if (amount >= sarThreshold) {
// In practice, this would include more sophisticated pattern analysis
// For now, just check if it's a large transaction to/from new addresses
KYCRecord memory fromRecord = kycRecords[from];
KYCRecord memory toRecord = kycRecords[to];
if (fromRecord.whitelistTime == 0 || toRecord.whitelistTime == 0) {
return (true, 2);
}
// Check if accounts were recently whitelisted (< 7 days)
if (block.timestamp - fromRecord.whitelistTime < 7 days ||
block.timestamp - toRecord.whitelistTime < 7 days) {
return (true, 2);
}
}
return (false, 0);
}
/**
* @dev Get KYC record for address
*/
function getKYCRecord(address account) external view override returns (KYCRecord memory) {
return kycRecords[account];
}
/**
* @dev Get velocity limits for address
*/
function getVelocityLimits(address account) external view override returns (VelocityLimits memory) {
return velocityLimits[account];
}
/**
* @dev Update reporting thresholds
*/
function updateReportingThresholds(
uint256 newCtrThreshold,
uint256 newSarThreshold
) external onlyRole(AML_MANAGER_ROLE) {
require(newCtrThreshold > 0, "CTR threshold must be positive");
require(newSarThreshold > 0, "SAR threshold must be positive");
ctrThreshold = newCtrThreshold;
sarThreshold = newSarThreshold;
}
/**
* @dev Update KYC level limits
*/
function updateKYCLevelLimits(
uint256 kycLevel,
uint256 dailyLimit,
uint256 monthlyLimit
) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(kycLevel >= 1 && kycLevel <= 3, "Invalid KYC level");
require(dailyLimit > 0 && monthlyLimit > 0, "Limits must be positive");
require(monthlyLimit >= dailyLimit, "Monthly must be >= daily");
kycLevelDailyLimits[kycLevel] = dailyLimit;
kycLevelMonthlyLimits[kycLevel] = monthlyLimit;
}
/**
* @dev Pause compliance operations
*/
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_pause();
}
/**
* @dev Unpause compliance operations
*/
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
}

View File

@@ -0,0 +1,259 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/IReserveAggregator.sol";
import "../interfaces/ICompliance.sol";
/**
* @title MeridianToken
* @dev Core token contract implementing the Meridian Protocol Standard
* @notice Regulated entities can issue tokens backed by reserves with built-in compliance
*/
contract MeridianToken is ERC20, ERC20Permit, AccessControl, Pausable, ReentrancyGuard {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
// Reserve and compliance contracts
IReserveAggregator public reserveAggregator;
ICompliance public compliance;
// Token metadata
string public currency; // e.g., "GBP", "USD", "EUR"
string public issuerName; // e.g., "Meridian LLC", "HSBC"
uint256 public minimumCollateralRatio; // e.g., 102% = 10200 (basis points * 100)
// Minting controls
uint256 public dailyMintLimit;
uint256 public dailyBurnLimit;
mapping(uint256 => uint256) public dailyMinted; // day => amount minted
mapping(uint256 => uint256) public dailyBurned; // day => amount burned
// Events
event Minted(address indexed to, uint256 amount, bytes32 attestationHash);
event Burned(address indexed from, uint256 amount, bytes32 attestationHash);
event ReserveAggregatorUpdated(address indexed newAggregator);
event ComplianceUpdated(address indexed newCompliance);
event DailyLimitsUpdated(uint256 newMintLimit, uint256 newBurnLimit);
event MinimumCollateralRatioUpdated(uint256 newRatio);
/**
* @dev Constructor sets up the token with initial parameters
* @param name Token name (e.g., "Meridian GBP")
* @param symbol Token symbol (e.g., "MGBP")
* @param _currency Currency denomination (e.g., "GBP")
* @param _issuerName Name of issuing entity
* @param _reserveAggregator Address of reserve aggregator contract
* @param _compliance Address of compliance contract
* @param _admin Address to receive admin role
*/
constructor(
string memory name,
string memory symbol,
string memory _currency,
string memory _issuerName,
address _reserveAggregator,
address _compliance,
address _admin
) ERC20(name, symbol) ERC20Permit(name) {
require(_reserveAggregator != address(0), "Invalid reserve aggregator");
require(_compliance != address(0), "Invalid compliance contract");
require(_admin != address(0), "Invalid admin address");
currency = _currency;
issuerName = _issuerName;
reserveAggregator = IReserveAggregator(_reserveAggregator);
compliance = ICompliance(_compliance);
minimumCollateralRatio = 10200; // 102% default
dailyMintLimit = 10000000 * 10**decimals(); // 10M default
dailyBurnLimit = 10000000 * 10**decimals(); // 10M default
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
_grantRole(MINTER_ROLE, _admin);
_grantRole(BURNER_ROLE, _admin);
_grantRole(COMPLIANCE_ROLE, _admin);
_grantRole(PAUSER_ROLE, _admin);
}
/**
* @dev Mint tokens with reserve attestation and compliance checks
* @param to Address to mint tokens to
* @param amount Amount to mint
* @param attestationHash Hash of reserve attestation document
*/
function mint(
address to,
uint256 amount,
bytes32 attestationHash
) external onlyRole(MINTER_ROLE) nonReentrant whenNotPaused {
require(to != address(0), "Cannot mint to zero address");
require(amount > 0, "Amount must be positive");
// Check daily mint limit
uint256 today = block.timestamp / 86400; // days since epoch
require(dailyMinted[today] + amount <= dailyMintLimit, "Daily mint limit exceeded");
// Check compliance
require(compliance.isWhitelisted(to), "Recipient not whitelisted");
require(!compliance.isSanctioned(to), "Recipient is sanctioned");
// Check reserve collateralization after mint
uint256 newTotalSupply = totalSupply() + amount;
(uint256 reserveValue, bool isValid) = reserveAggregator.getTotalReserveValue();
require(isValid, "Reserve data is stale");
uint256 requiredReserves = (newTotalSupply * minimumCollateralRatio) / 10000;
require(reserveValue >= requiredReserves, "Insufficient reserves for mint");
// Update daily tracking
dailyMinted[today] += amount;
// Mint tokens
_mint(to, amount);
emit Minted(to, amount, attestationHash);
}
/**
* @dev Burn tokens with attestation
* @param from Address to burn tokens from
* @param amount Amount to burn
* @param attestationHash Hash of burn attestation document
*/
function burn(
address from,
uint256 amount,
bytes32 attestationHash
) external onlyRole(BURNER_ROLE) nonReentrant whenNotPaused {
require(from != address(0), "Cannot burn from zero address");
require(amount > 0, "Amount must be positive");
require(balanceOf(from) >= amount, "Insufficient balance");
// Check daily burn limit
uint256 today = block.timestamp / 86400;
require(dailyBurned[today] + amount <= dailyBurnLimit, "Daily burn limit exceeded");
// Update daily tracking
dailyBurned[today] += amount;
// Burn tokens
_burn(from, amount);
emit Burned(from, amount, attestationHash);
}
/**
* @dev Override transfer to include compliance checks
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);
// Skip compliance for minting/burning (handled in mint/burn functions)
if (from == address(0) || to == address(0)) {
return;
}
// Check compliance for regular transfers
require(!paused(), "Token transfers paused");
require(compliance.isWhitelisted(to), "Recipient not whitelisted");
require(!compliance.isSanctioned(from), "Sender is sanctioned");
require(!compliance.isSanctioned(to), "Recipient is sanctioned");
// Check velocity limits
require(compliance.checkVelocityLimits(from, amount), "Velocity limit exceeded");
}
/**
* @dev Get current collateralization ratio
* @return ratio Current ratio in basis points * 100 (e.g., 10200 = 102%)
* @return isValid Whether the reserve data is current and valid
*/
function getCurrentCollateralizationRatio() external view returns (uint256 ratio, bool isValid) {
(uint256 reserveValue, bool _isValid) = reserveAggregator.getTotalReserveValue();
if (!_isValid || totalSupply() == 0) {
return (0, false);
}
ratio = (reserveValue * 10000) / totalSupply();
return (ratio, true);
}
/**
* @dev Update reserve aggregator contract
*/
function updateReserveAggregator(address newAggregator) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newAggregator != address(0), "Invalid aggregator address");
reserveAggregator = IReserveAggregator(newAggregator);
emit ReserveAggregatorUpdated(newAggregator);
}
/**
* @dev Update compliance contract
*/
function updateCompliance(address newCompliance) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newCompliance != address(0), "Invalid compliance address");
compliance = ICompliance(newCompliance);
emit ComplianceUpdated(newCompliance);
}
/**
* @dev Update daily mint/burn limits
*/
function updateDailyLimits(
uint256 newMintLimit,
uint256 newBurnLimit
) external onlyRole(DEFAULT_ADMIN_ROLE) {
dailyMintLimit = newMintLimit;
dailyBurnLimit = newBurnLimit;
emit DailyLimitsUpdated(newMintLimit, newBurnLimit);
}
/**
* @dev Update minimum collateral ratio
*/
function updateMinimumCollateralRatio(uint256 newRatio) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newRatio >= 10000, "Ratio must be at least 100%");
minimumCollateralRatio = newRatio;
emit MinimumCollateralRatioUpdated(newRatio);
}
/**
* @dev Pause token operations
*/
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
}
/**
* @dev Unpause token operations
*/
function unpause() external onlyRole(PAUSER_ROLE) {
_unpause();
}
/**
* @dev Get remaining mint capacity for today
*/
function getRemainingMintCapacity() external view returns (uint256) {
uint256 today = block.timestamp / 86400;
return dailyMintLimit - dailyMinted[today];
}
/**
* @dev Get remaining burn capacity for today
*/
function getRemainingBurnCapacity() external view returns (uint256) {
uint256 today = block.timestamp / 86400;
return dailyBurnLimit - dailyBurned[today];
}
}

View File

@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title ICompliance
* @dev Interface for KYC, sanctions screening, and AML compliance
*/
interface ICompliance {
struct KYCRecord {
bool isWhitelisted;
uint256 kycLevel; // 1=basic, 2=enhanced, 3=institutional
uint256 whitelistTime;
uint256 expiryTime;
string jurisdiction;
}
struct VelocityLimits {
uint256 dailyLimit;
uint256 monthlyLimit;
uint256 dailySpent;
uint256 monthlySpent;
uint256 lastDayReset;
uint256 lastMonthReset;
}
/**
* @dev Check if address is whitelisted for KYC
* @param account Address to check
* @return isWhitelisted True if account passed KYC
*/
function isWhitelisted(address account) external view returns (bool);
/**
* @dev Check if address is on sanctions list
* @param account Address to check
* @return isSanctioned True if account is sanctioned
*/
function isSanctioned(address account) external view returns (bool);
/**
* @dev Check velocity limits for transaction
* @param account Address making transaction
* @param amount Transaction amount
* @return withinLimits True if transaction is within velocity limits
*/
function checkVelocityLimits(address account, uint256 amount) external view returns (bool withinLimits);
/**
* @dev Get KYC record for address
* @param account Address to query
*/
function getKYCRecord(address account) external view returns (KYCRecord memory);
/**
* @dev Get velocity limits for address
* @param account Address to query
*/
function getVelocityLimits(address account) external view returns (VelocityLimits memory);
/**
* @dev Update velocity tracking after transaction
* @param account Address that made transaction
* @param amount Transaction amount
*/
function updateVelocityTracking(address account, uint256 amount) external;
/**
* @dev Check if transaction should trigger AML reporting
* @param from Sender address
* @param to Recipient address
* @param amount Transaction amount
* @return shouldReport True if transaction exceeds reporting thresholds
* @return reportType Type of report needed (1=CTR, 2=SAR, etc.)
*/
function checkReportingThresholds(
address from,
address to,
uint256 amount
) external view returns (bool shouldReport, uint8 reportType);
// Events
event AccountWhitelisted(address indexed account, uint256 kycLevel, string jurisdiction);
event AccountBlacklisted(address indexed account, string reason);
event SanctionsListUpdated(uint256 newListHash);
event VelocityLimitsUpdated(address indexed account, uint256 dailyLimit, uint256 monthlyLimit);
event ReportingThresholdTriggered(address indexed from, address indexed to, uint256 amount, uint8 reportType);
event VelocityLimitExceeded(address indexed account, uint256 attempted, uint256 remaining);
}

View File

@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title IReserveAggregator
* @dev Interface for reserve aggregation from multiple custodians
*/
interface IReserveAggregator {
struct CustodianInfo {
string name;
address oracle;
uint256 lastUpdateTime;
uint256 heartbeat; // maximum staleness in seconds
bool isActive;
uint8 custodianType; // 0=bank, 1=crypto, 2=fund_admin
}
struct ReserveAttestation {
uint256 balance;
uint256 timestamp;
bytes32 documentHash;
address attestor;
bool isValid;
}
/**
* @dev Get total reserve value across all active custodians
* @return totalValue Total reserve value in token denomination
* @return isValid Whether all required custodian data is current
*/
function getTotalReserveValue() external view returns (uint256 totalValue, bool isValid);
/**
* @dev Get reserve value from specific custodian
* @param custodianId Unique identifier for custodian
* @return value Reserve value from this custodian
* @return isValid Whether this custodian's data is current
*/
function getCustodianReserveValue(bytes32 custodianId) external view returns (uint256 value, bool isValid);
/**
* @dev Get custodian information
* @param custodianId Unique identifier for custodian
*/
function getCustodianInfo(bytes32 custodianId) external view returns (CustodianInfo memory);
/**
* @dev Get latest attestation for a custodian
* @param custodianId Unique identifier for custodian
*/
function getLatestAttestation(bytes32 custodianId) external view returns (ReserveAttestation memory);
/**
* @dev Check if reserve data is stale for any custodian
* @return isStale True if any custodian data exceeds heartbeat
* @return staleCustodians Array of custodian IDs with stale data
*/
function checkDataStaleness() external view returns (bool isStale, bytes32[] memory staleCustodians);
// Events
event CustodianAdded(bytes32 indexed custodianId, string name, address oracle);
event CustodianUpdated(bytes32 indexed custodianId, address newOracle, uint256 newHeartbeat);
event CustodianDeactivated(bytes32 indexed custodianId);
event ReserveAttested(bytes32 indexed custodianId, uint256 balance, bytes32 documentHash);
event StaleDataDetected(bytes32 indexed custodianId, uint256 lastUpdate, uint256 heartbeat);
}