- 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
260 lines
9.6 KiB
Solidity
260 lines
9.6 KiB
Solidity
// 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];
|
|
}
|
|
}
|