// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./interfaces/IPolicyManager.sol"; import "./interfaces/IComplianceRegistry.sol"; import "./interfaces/IDebtRegistry.sol"; import "./libraries/ReasonCodes.sol"; import "./errors/RegistryErrors.sol"; /** * @title PolicyManager * @notice Central rule engine for transfer authorization across all eMoney tokens * @dev Consults ComplianceRegistry and DebtRegistry to make transfer decisions. * Manages per-token configuration including pause state, bridge-only mode, and lien modes. * Lien enforcement is performed by eMoneyToken based on the lien mode returned here. */ contract PolicyManager is IPolicyManager, AccessControl { bytes32 public constant POLICY_OPERATOR_ROLE = keccak256("POLICY_OPERATOR_ROLE"); struct TokenConfig { bool paused; bool bridgeOnly; address bridge; uint8 lienMode; // 0 = off, 1 = hard freeze, 2 = encumbered } IComplianceRegistry public immutable complianceRegistry; IDebtRegistry public immutable debtRegistry; mapping(address => TokenConfig) private _tokenConfigs; mapping(address => mapping(address => bool)) private _tokenFreezes; // token => account => frozen /** * @notice Initializes PolicyManager with registry addresses * @param admin Address that will receive DEFAULT_ADMIN_ROLE * @param compliance Address of ComplianceRegistry contract * @param debt Address of DebtRegistry contract */ constructor(address admin, address compliance, address debt) { _grantRole(DEFAULT_ADMIN_ROLE, admin); complianceRegistry = IComplianceRegistry(compliance); debtRegistry = IDebtRegistry(debt); } /** * @notice Returns whether a token is paused * @param token Token address to check * @return true if token is paused, false otherwise */ function isPaused(address token) external view override returns (bool) { return _tokenConfigs[token].paused; } /** * @notice Returns whether a token is in bridge-only mode * @param token Token address to check * @return true if token only allows transfers to/from bridge, false otherwise */ function bridgeOnly(address token) external view override returns (bool) { return _tokenConfigs[token].bridgeOnly; } /** * @notice Returns the bridge address for a token * @param token Token address to check * @return Bridge address (zero address if not set) */ function bridge(address token) external view override returns (address) { return _tokenConfigs[token].bridge; } /** * @notice Returns the lien mode for a token * @param token Token address to check * @return Lien mode: 0 = off, 1 = hard freeze, 2 = encumbered */ function lienMode(address token) external view override returns (uint8) { return _tokenConfigs[token].lienMode; } /** * @notice Returns whether an account is frozen for a specific token * @param token Token address to check * @param account Address to check * @return true if account is frozen for this token, false otherwise */ function isTokenFrozen(address token, address account) external view override returns (bool) { return _tokenFreezes[token][account]; } /** * @notice Determines if a transfer should be allowed * @dev Checks in order: paused, token freezes, compliance freezes, compliance allowed status, bridge-only mode. * Lien checks are performed by eMoneyToken based on lien mode. * @param token Token address * @param from Sender address * @param to Recipient address * @param amount Transfer amount (unused but required for interface) * @return allowed true if transfer should be allowed, false otherwise * @return reasonCode bytes32 reason code (ReasonCodes.OK if allowed, otherwise the blocking reason) */ function canTransfer( address token, address from, address to, uint256 amount ) external view override returns (bool allowed, bytes32 reasonCode) { TokenConfig memory config = _tokenConfigs[token]; // Check paused if (config.paused) { return (false, ReasonCodes.PAUSED); } // Check token-specific freezes if (_tokenFreezes[token][from]) { return (false, ReasonCodes.FROM_FROZEN); } if (_tokenFreezes[token][to]) { return (false, ReasonCodes.TO_FROZEN); } // Check compliance registry freezes if (complianceRegistry.isFrozen(from)) { return (false, ReasonCodes.FROM_FROZEN); } if (complianceRegistry.isFrozen(to)) { return (false, ReasonCodes.TO_FROZEN); } // Check compliance allowed status if (!complianceRegistry.isAllowed(from)) { return (false, ReasonCodes.FROM_NOT_COMPLIANT); } if (!complianceRegistry.isAllowed(to)) { return (false, ReasonCodes.TO_NOT_COMPLIANT); } // Check bridgeOnly mode if (config.bridgeOnly) { if (from != config.bridge && to != config.bridge) { return (false, ReasonCodes.BRIDGE_ONLY); } } // Lien mode checks are handled in eMoneyToken._update // PolicyManager only provides the lien mode, not the enforcement return (true, ReasonCodes.OK); } /** * @notice Check if transfer is allowed with additional context * @param token Token address * @param from Sender address * @param to Recipient address * @param amount Transfer amount * @param context Additional context data (unused but required by interface) * @return allowed True if transfer is allowed * @return reasonCode Reason code if not allowed */ function canTransferWithContext( address token, address from, address to, uint256 amount, bytes memory context ) external view override returns (bool allowed, bytes32 reasonCode) { // For now, context is unused - use same logic as canTransfer TokenConfig memory config = _tokenConfigs[token]; // Check paused if (config.paused) { return (false, ReasonCodes.PAUSED); } // Check token-specific freezes if (_tokenFreezes[token][from]) { return (false, ReasonCodes.FROM_FROZEN); } if (_tokenFreezes[token][to]) { return (false, ReasonCodes.TO_FROZEN); } // Check compliance registry freezes if (complianceRegistry.isFrozen(from)) { return (false, ReasonCodes.FROM_FROZEN); } if (complianceRegistry.isFrozen(to)) { return (false, ReasonCodes.TO_FROZEN); } // Check compliance allowed status if (!complianceRegistry.isAllowed(from)) { return (false, ReasonCodes.FROM_NOT_COMPLIANT); } if (!complianceRegistry.isAllowed(to)) { return (false, ReasonCodes.TO_NOT_COMPLIANT); } // Check bridgeOnly mode if (config.bridgeOnly) { if (from != config.bridge && to != config.bridge) { return (false, ReasonCodes.BRIDGE_ONLY); } } return (true, ReasonCodes.OK); } /** * @notice Sets the paused state for a token * @dev Requires POLICY_OPERATOR_ROLE. When paused, all transfers are blocked. * @param token Token address to configure * @param paused true to pause, false to unpause */ function setPaused(address token, bool paused) external override onlyRole(POLICY_OPERATOR_ROLE) { _tokenConfigs[token].paused = paused; emit TokenPaused(token, paused); } /** * @notice Sets bridge-only mode for a token * @dev Requires POLICY_OPERATOR_ROLE. When enabled, only transfers to/from the bridge address are allowed. * @param token Token address to configure * @param enabled true to enable bridge-only mode, false to disable */ function setBridgeOnly(address token, bool enabled) external override onlyRole(POLICY_OPERATOR_ROLE) { _tokenConfigs[token].bridgeOnly = enabled; emit BridgeOnlySet(token, enabled); } /** * @notice Sets the bridge address for a token * @dev Requires POLICY_OPERATOR_ROLE. Used in bridge-only mode. * @param token Token address to configure * @param bridgeAddr Address of the bridge contract */ function setBridge(address token, address bridgeAddr) external override onlyRole(POLICY_OPERATOR_ROLE) { _tokenConfigs[token].bridge = bridgeAddr; emit BridgeSet(token, bridgeAddr); } /** * @notice Sets the lien mode for a token * @dev Requires POLICY_OPERATOR_ROLE. Valid modes: 0 = off, 1 = hard freeze, 2 = encumbered. * @param token Token address to configure * @param mode Lien mode (0, 1, or 2) */ function setLienMode(address token, uint8 mode) external override onlyRole(POLICY_OPERATOR_ROLE) { if (mode > 2) revert PolicyInvalidLienMode(mode); TokenConfig storage config = _tokenConfigs[token]; bool isNewToken = config.lienMode == 0 && mode != 0 && !config.paused && !config.bridgeOnly && config.bridge == address(0); config.lienMode = mode; if (isNewToken) { emit TokenConfigured(token, config.paused, config.bridgeOnly, config.bridge, mode); } emit LienModeSet(token, mode); } /** * @notice Freezes or unfreezes an account for a specific token * @dev Requires POLICY_OPERATOR_ROLE. Per-token freeze (in addition to global compliance freezes). * @param token Token address * @param account Address to freeze/unfreeze * @param frozen true to freeze, false to unfreeze */ function freeze(address token, address account, bool frozen) external override onlyRole(POLICY_OPERATOR_ROLE) { _tokenFreezes[token][account] = frozen; emit TokenFreeze(token, account, frozen); } }