232 lines
8.6 KiB
Solidity
232 lines
8.6 KiB
Solidity
// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||
import "./interfaces/ICoupon.sol";
|
||
|
||
/// @title Compliance — 合规合约
|
||
/// @notice OFAC 黑名单筛查、Travel Rule、KYC 差异化检查、账户冻结
|
||
/// @dev 使用 Transparent Proxy 部署
|
||
contract Compliance is Initializable, AccessControlUpgradeable {
|
||
bytes32 public constant COMPLIANCE_OFFICER_ROLE = keccak256("COMPLIANCE_OFFICER_ROLE");
|
||
bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE");
|
||
bytes32 public constant KYC_PROVIDER_ROLE = keccak256("KYC_PROVIDER_ROLE");
|
||
|
||
// OFAC 黑名单
|
||
mapping(address => bool) private _blacklist;
|
||
// 冻结账户(紧急冻结,需 Governance 多签)
|
||
mapping(address => bool) private _frozen;
|
||
// KYC 等级映射: 0=L0(未验证), 1=L1(基本), 2=L2(增强), 3=L3(机构)
|
||
mapping(address => uint8) private _kycLevels;
|
||
// Travel Rule 记录
|
||
mapping(address => mapping(address => bool)) private _travelRuleRecorded;
|
||
// Travel Rule 详情
|
||
mapping(bytes32 => TravelRuleRecord) private _travelRuleRecords;
|
||
|
||
struct TravelRuleRecord {
|
||
address sender;
|
||
address receiver;
|
||
bytes32 senderInfoHash;
|
||
bytes32 receiverInfoHash;
|
||
uint256 recordedAt;
|
||
}
|
||
|
||
// Travel Rule 阈值(美元,USDC 精度 6 位)
|
||
uint256 public constant TRAVEL_RULE_THRESHOLD = 3000e6; // $3,000
|
||
// 大额交易阈值
|
||
uint256 public constant LARGE_TRADE_THRESHOLD = 10000e6; // $10,000
|
||
|
||
// --- Events ---
|
||
event AddressBlacklisted(address indexed account, string reason);
|
||
event AddressRemovedFromBlacklist(address indexed account);
|
||
event AccountFrozen(address indexed account);
|
||
event AccountUnfrozen(address indexed account);
|
||
event KycLevelUpdated(address indexed account, uint8 oldLevel, uint8 newLevel);
|
||
event TravelRuleRecorded(
|
||
address indexed sender,
|
||
address indexed receiver,
|
||
bytes32 senderInfoHash,
|
||
bytes32 receiverInfoHash
|
||
);
|
||
|
||
function initialize(address admin) external initializer {
|
||
__AccessControl_init();
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(COMPLIANCE_OFFICER_ROLE, admin);
|
||
_grantRole(GOVERNANCE_ROLE, admin);
|
||
_grantRole(KYC_PROVIDER_ROLE, admin);
|
||
}
|
||
|
||
// ========================
|
||
// OFAC 黑名单
|
||
// ========================
|
||
|
||
/// @notice OFAC 筛查
|
||
function isBlacklisted(address account) external view returns (bool) {
|
||
return _blacklist[account];
|
||
}
|
||
|
||
/// @notice 添加 OFAC 黑名单
|
||
function addToBlacklist(address account, string calldata reason)
|
||
external
|
||
onlyRole(COMPLIANCE_OFFICER_ROLE)
|
||
{
|
||
_blacklist[account] = true;
|
||
emit AddressBlacklisted(account, reason);
|
||
}
|
||
|
||
/// @notice 从黑名单移除
|
||
function removeFromBlacklist(address account) external onlyRole(GOVERNANCE_ROLE) {
|
||
_blacklist[account] = false;
|
||
emit AddressRemovedFromBlacklist(account);
|
||
}
|
||
|
||
// ========================
|
||
// 账户冻结
|
||
// ========================
|
||
|
||
/// @notice 查询是否冻结
|
||
function isFrozen(address account) external view returns (bool) {
|
||
return _frozen[account];
|
||
}
|
||
|
||
/// @notice 紧急冻结(需 Governance 多签)
|
||
function freezeAccount(address account) external onlyRole(GOVERNANCE_ROLE) {
|
||
_frozen[account] = true;
|
||
emit AccountFrozen(account);
|
||
}
|
||
|
||
/// @notice 解除冻结
|
||
function unfreezeAccount(address account) external onlyRole(GOVERNANCE_ROLE) {
|
||
_frozen[account] = false;
|
||
emit AccountUnfrozen(account);
|
||
}
|
||
|
||
// ========================
|
||
// KYC 等级管理
|
||
// ========================
|
||
|
||
/// @notice 获取 KYC 等级
|
||
function getKycLevel(address account) external view returns (uint8) {
|
||
return _kycLevels[account];
|
||
}
|
||
|
||
/// @notice 设置 KYC 等级
|
||
function setKycLevel(address account, uint8 level) external onlyRole(KYC_PROVIDER_ROLE) {
|
||
require(level <= 3, "Compliance: invalid KYC level");
|
||
uint8 oldLevel = _kycLevels[account];
|
||
_kycLevels[account] = level;
|
||
emit KycLevelUpdated(account, oldLevel, level);
|
||
}
|
||
|
||
/// @notice 批量设置 KYC 等级
|
||
function batchSetKycLevel(address[] calldata accounts, uint8[] calldata levels)
|
||
external
|
||
onlyRole(KYC_PROVIDER_ROLE)
|
||
{
|
||
require(accounts.length == levels.length, "Compliance: length mismatch");
|
||
for (uint256 i = 0; i < accounts.length; i++) {
|
||
require(levels[i] <= 3, "Compliance: invalid KYC level");
|
||
uint8 oldLevel = _kycLevels[accounts[i]];
|
||
_kycLevels[accounts[i]] = levels[i];
|
||
emit KycLevelUpdated(accounts[i], oldLevel, levels[i]);
|
||
}
|
||
}
|
||
|
||
/// @notice 要求最低 KYC 等级(revert if不满足)
|
||
function requireKycLevel(address account, uint8 requiredLevel) external view {
|
||
require(_kycLevels[account] >= requiredLevel, "Compliance: insufficient KYC level");
|
||
}
|
||
|
||
// ========================
|
||
// Travel Rule
|
||
// ========================
|
||
|
||
/// @notice 记录 Travel Rule 数据(≥$3,000 强制)
|
||
function recordTravelRule(
|
||
address sender,
|
||
address receiver,
|
||
bytes32 senderInfoHash,
|
||
bytes32 receiverInfoHash
|
||
) external onlyRole(COMPLIANCE_OFFICER_ROLE) {
|
||
require(sender != address(0) && receiver != address(0), "Compliance: zero address");
|
||
require(senderInfoHash != bytes32(0) && receiverInfoHash != bytes32(0), "Compliance: empty hash");
|
||
|
||
_travelRuleRecorded[sender][receiver] = true;
|
||
|
||
bytes32 recordId = keccak256(abi.encodePacked(sender, receiver, block.timestamp));
|
||
_travelRuleRecords[recordId] = TravelRuleRecord({
|
||
sender: sender,
|
||
receiver: receiver,
|
||
senderInfoHash: senderInfoHash,
|
||
receiverInfoHash: receiverInfoHash,
|
||
recordedAt: block.timestamp
|
||
});
|
||
|
||
emit TravelRuleRecorded(sender, receiver, senderInfoHash, receiverInfoHash);
|
||
}
|
||
|
||
/// @notice 查询 Travel Rule 是否已记录
|
||
function hasTravelRuleRecord(address sender, address receiver) external view returns (bool) {
|
||
return _travelRuleRecorded[sender][receiver];
|
||
}
|
||
|
||
// ========================
|
||
// 综合合规检查
|
||
// ========================
|
||
|
||
/// @notice 交易前合规检查(Settlement 调用)
|
||
function preTradeCheck(
|
||
address buyer,
|
||
address seller,
|
||
uint256 amount,
|
||
ICoupon.CouponType couponType
|
||
) external view {
|
||
// OFAC 黑名单检查
|
||
require(!_blacklist[buyer], "Compliance: buyer blacklisted");
|
||
require(!_blacklist[seller], "Compliance: seller blacklisted");
|
||
|
||
// 冻结检查
|
||
require(!_frozen[buyer], "Compliance: buyer frozen");
|
||
require(!_frozen[seller], "Compliance: seller frozen");
|
||
|
||
// Utility Track: 双方至少 KYC L1
|
||
if (couponType == ICoupon.CouponType.Utility) {
|
||
require(_kycLevels[buyer] >= 1, "Compliance: buyer needs KYC L1 for Utility");
|
||
require(_kycLevels[seller] >= 1, "Compliance: seller needs KYC L1 for Utility");
|
||
}
|
||
// Securities Track: 双方至少 KYC L2
|
||
else {
|
||
require(_kycLevels[buyer] >= 2, "Compliance: buyer needs KYC L2 for Security");
|
||
require(_kycLevels[seller] >= 2, "Compliance: seller needs KYC L2 for Security");
|
||
}
|
||
|
||
// 大额交易: KYC L2+
|
||
if (amount >= LARGE_TRADE_THRESHOLD) {
|
||
require(_kycLevels[buyer] >= 2, "Compliance: buyer needs KYC L2 for large trade");
|
||
require(_kycLevels[seller] >= 2, "Compliance: seller needs KYC L2 for large trade");
|
||
}
|
||
}
|
||
|
||
/// @notice P2P 转移合规路由
|
||
function p2pComplianceCheck(
|
||
address sender,
|
||
address receiver,
|
||
uint256 amount
|
||
) external view {
|
||
require(!_blacklist[sender], "Compliance: sender blacklisted");
|
||
require(!_blacklist[receiver], "Compliance: receiver blacklisted");
|
||
require(!_frozen[sender], "Compliance: sender frozen");
|
||
require(!_frozen[receiver], "Compliance: receiver frozen");
|
||
|
||
// ≥$3,000 强制 Travel Rule
|
||
if (amount >= TRAVEL_RULE_THRESHOLD) {
|
||
require(_kycLevels[sender] >= 2, "Compliance: sender needs KYC L2 for Travel Rule");
|
||
require(_kycLevels[receiver] >= 2, "Compliance: receiver needs KYC L2 for Travel Rule");
|
||
require(_travelRuleRecorded[sender][receiver], "Compliance: Travel Rule data required");
|
||
}
|
||
}
|
||
}
|