// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "./interfaces/IeMoneyToken.sol"; import "./interfaces/IPolicyManager.sol"; import "./interfaces/IDebtRegistry.sol"; import "./interfaces/IComplianceRegistry.sol"; import "./errors/TokenErrors.sol"; import "./libraries/ReasonCodes.sol"; /** * @title eMoneyToken * @notice Restricted ERC-20 token with policy-controlled transfers and lien enforcement * @dev Implements UUPS upgradeable proxy pattern. All transfers are validated through PolicyManager. * Supports two lien enforcement modes: hard freeze (blocks all transfers with liens) and encumbered * (allows transfers up to freeBalance = balance - activeLienAmount). */ contract eMoneyToken is Initializable, ERC20Upgradeable, AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable, IeMoneyToken { bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE"); bytes32 public constant ENFORCEMENT_ROLE = keccak256("ENFORCEMENT_ROLE"); IPolicyManager public policyManager; IDebtRegistry public debtRegistry; IComplianceRegistry public complianceRegistry; uint8 private _decimals; bool private _inForceTransfer; bool private _inClawback; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /** * @notice Initializes the token with configuration * @dev Called once during proxy deployment. Can only be called once. * @param name Token name (e.g., "eMoney Token") * @param symbol Token symbol (e.g., "EMT") * @param decimals_ Number of decimals (typically 18) * @param issuer Address that will receive ISSUER_ROLE and DEFAULT_ADMIN_ROLE * @param policyManager_ Address of PolicyManager contract * @param debtRegistry_ Address of DebtRegistry contract * @param complianceRegistry_ Address of ComplianceRegistry contract */ function initialize( string calldata name, string calldata symbol, uint8 decimals_, address issuer, address policyManager_, address debtRegistry_, address complianceRegistry_ ) external initializer { __ERC20_init(name, symbol); __AccessControl_init(); __UUPSUpgradeable_init(); __ReentrancyGuard_init(); _decimals = decimals_; policyManager = IPolicyManager(policyManager_); debtRegistry = IDebtRegistry(debtRegistry_); complianceRegistry = IComplianceRegistry(complianceRegistry_); _grantRole(DEFAULT_ADMIN_ROLE, issuer); _grantRole(ISSUER_ROLE, issuer); } /** * @notice Returns the number of decimals for the token * @return Number of decimals (typically 18) */ function decimals() public view virtual override returns (uint8) { return _decimals; } /** * @notice Returns the free balance available for transfer (balance minus active encumbrances) * @dev In encumbered mode, transfers are limited to freeBalance * @param account Address to check * @return Free balance (balanceOf - activeLienAmount, floored at 0) */ function freeBalanceOf(address account) external view override returns (uint256) { uint256 balance = balanceOf(account); uint256 encumbrance = debtRegistry.activeLienAmount(account); return balance > encumbrance ? balance - encumbrance : 0; } /** * @notice Internal hook that enforces transfer restrictions before updating balances * @dev Overrides ERC20Upgradeable._update to add policy checks and lien enforcement. * Skips checks for mint/burn operations (from/to == address(0)) and privileged operations * (clawback, forceTransfer). * @param from Sender address (address(0) for mints) * @param to Recipient address (address(0) for burns) * @param amount Transfer amount */ function _update( address from, address to, uint256 amount ) internal virtual override { // Skip checks for privileged operations (mint/burn internal transfers) if (from == address(0) || to == address(0)) { super._update(from, to, amount); return; } // Skip all checks during clawback (bypasses everything) if (_inClawback) { super._update(from, to, amount); return; } // Skip lien checks during forceTransfer (compliance already checked there) if (_inForceTransfer) { super._update(from, to, amount); return; } // Check policy manager (bool allowed, bytes32 reasonCode) = policyManager.canTransfer(address(this), from, to, amount); if (!allowed) { revert TransferBlocked(reasonCode, from, to, amount); } // Check lien mode enforcement uint8 mode = policyManager.lienMode(address(this)); if (mode == 1) { // Hard freeze mode: any active lien blocks all transfers if (debtRegistry.hasActiveLien(from)) { revert TransferBlocked(ReasonCodes.LIEN_BLOCK, from, to, amount); } } else if (mode == 2) { // Encumbered mode: allow transfers up to freeBalance uint256 encumbrance = debtRegistry.activeLienAmount(from); uint256 balance = balanceOf(from); uint256 freeBalance = balance > encumbrance ? balance - encumbrance : 0; if (amount > freeBalance) { revert TransferBlocked(ReasonCodes.INSUFF_FREE_BAL, from, to, amount); } } // mode == 0: no lien enforcement super._update(from, to, amount); } /** * @notice Mints new tokens to an account * @dev Requires ISSUER_ROLE. Bypasses all transfer restrictions (mint operation). * @param to Recipient address * @param amount Amount to mint * @param reasonCode Reason code for the mint operation (e.g., ReasonCodes.OK) */ function mint(address to, uint256 amount, bytes32 reasonCode) external override onlyRole(ISSUER_ROLE) nonReentrant { _mint(to, amount); emit Minted(to, amount, reasonCode); } /** * @notice Burns tokens from an account * @dev Requires ISSUER_ROLE. Bypasses all transfer restrictions (burn operation). * @param from Account to burn from * @param amount Amount to burn * @param reasonCode Reason code for the burn operation (e.g., ReasonCodes.OK) */ function burn(address from, uint256 amount, bytes32 reasonCode) external override onlyRole(ISSUER_ROLE) nonReentrant { _burn(from, amount); emit Burned(from, amount, reasonCode); } /** * @notice Clawback transfers tokens, bypassing all restrictions * @dev Requires ENFORCEMENT_ROLE. Bypasses all checks including liens, compliance, and policy. * Used for emergency recovery or enforcement actions. * @param from Source address * @param to Destination address * @param amount Amount to transfer * @param reasonCode Reason code for the clawback operation */ function clawback( address from, address to, uint256 amount, bytes32 reasonCode ) external override onlyRole(ENFORCEMENT_ROLE) nonReentrant { // Clawback bypasses all checks including liens and compliance _inClawback = true; _transfer(from, to, amount); _inClawback = false; emit Clawback(from, to, amount, reasonCode); } /** * @notice Force transfer bypasses liens but enforces compliance * @dev Requires ENFORCEMENT_ROLE. Bypasses lien enforcement but still checks compliance. * Used when liens need to be bypassed but compliance must still be enforced. * @param from Source address * @param to Destination address * @param amount Amount to transfer * @param reasonCode Reason code for the force transfer operation */ function forceTransfer( address from, address to, uint256 amount, bytes32 reasonCode ) external override onlyRole(ENFORCEMENT_ROLE) nonReentrant { // ForceTransfer bypasses liens but still enforces compliance // Check compliance if (!complianceRegistry.isAllowed(from)) { revert FromNotCompliant(from); } if (!complianceRegistry.isAllowed(to)) { revert ToNotCompliant(to); } if (complianceRegistry.isFrozen(from)) { revert FromFrozen(from); } if (complianceRegistry.isFrozen(to)) { revert ToFrozen(to); } // Set flag to bypass lien checks in _update _inForceTransfer = true; _transfer(from, to, amount); _inForceTransfer = false; emit ForcedTransfer(from, to, amount, reasonCode); } /** * @notice Authorizes an upgrade to a new implementation * @dev Internal function required by UUPSUpgradeable. Only DEFAULT_ADMIN_ROLE can authorize upgrades. * @param newImplementation Address of the new implementation contract */ function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} }