- 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
380 lines
13 KiB
Solidity
380 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 "../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();
|
|
}
|
|
}
|