// 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]; } }