// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "../compliance/libraries/MonetaryFormulas.sol"; import "./IGruMonetaryPolicyGate.sol"; /** * @title GruMonetaryPolicyGate * @notice Enforces GRU Monetary Policy guardrails before Li* / M00 mint. * @dev Operator updates metrics from `run-gru-monetary-metrics.mjs`; RWAToken mint calls canMint. */ contract GruMonetaryPolicyGate is IGruMonetaryPolicyGate, AccessControl { bytes32 public constant METRICS_PUBLISHER_ROLE = keccak256("METRICS_PUBLISHER_ROLE"); bytes32 public constant GATE_ADMIN_ROLE = keccak256("GATE_ADMIN_ROLE"); uint256 public immutable coverageMinBps; uint256 public immutable coverageAlertBps; uint256 public immutable maxM1ToM00Utilization; MetricsSnapshot private _metrics; mapping(address => bool) public override tokenGateEnabled; constructor( address admin, uint256 coverageMinBps_, uint256 coverageAlertBps_, uint256 maxM1ToM00Utilization_ ) { require(admin != address(0), "GruMonetaryPolicyGate: admin"); require(coverageMinBps_ >= coverageAlertBps_, "GruMonetaryPolicyGate: coverage"); require(maxM1ToM00Utilization_ > 0, "GruMonetaryPolicyGate: util"); coverageMinBps = coverageMinBps_; coverageAlertBps = coverageAlertBps_; maxM1ToM00Utilization = maxM1ToM00Utilization_; _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(GATE_ADMIN_ROLE, admin); _grantRole(METRICS_PUBLISHER_ROLE, admin); } function metrics() external view override returns (MetricsSnapshot memory) { return _metrics; } function setTokenGate(address token, bool enabled) external onlyRole(GATE_ADMIN_ROLE) { tokenGateEnabled[token] = enabled; emit PolicyGateSet(token, enabled); } function updateMetrics( uint256 coverageRatioBps, uint256 m1ToM00Utilization, VelocityZone zone, bool issuancePaused ) external onlyRole(METRICS_PUBLISHER_ROLE) { _metrics = MetricsSnapshot({ coverageRatioBps: coverageRatioBps, m1ToM00Utilization: m1ToM00Utilization, velocityZone: zone, metricsUpdatedAt: block.timestamp, issuancePaused: issuancePaused }); emit MetricsUpdated(coverageRatioBps, m1ToM00Utilization, zone, issuancePaused); } function canMint(address token, uint256) external view override returns (bool) { (bool ok,) = canMintReason(token, 0); return ok; } function canMintReason(address token, uint256) public view override returns (bool ok, bytes32 reason) { if (!tokenGateEnabled[token]) { return (true, bytes32(0)); } if (_metrics.issuancePaused) { return (false, keccak256("ISSUANCE_PAUSED")); } if (_metrics.velocityZone == VelocityZone.Black || _metrics.velocityZone == VelocityZone.Red) { return (false, keccak256("VELOCITY_ZONE_BLOCK")); } if (_metrics.coverageRatioBps < coverageAlertBps) { return (false, keccak256("COVERAGE_BELOW_ALERT")); } if (_metrics.m1ToM00Utilization > maxM1ToM00Utilization) { return (false, keccak256("M1_M00_UTILIZATION_EXCEEDED")); } if (_metrics.coverageRatioBps < coverageMinBps && _metrics.velocityZone >= VelocityZone.AmberHigh) { return (false, keccak256("COVERAGE_VELOCITY_COMBO")); } token; return (true, bytes32(0)); } /// @notice Pure helper mirroring off-chain metrics job (coverage bps). function computeCoverageBps(uint256 reserveValue, uint256 circulatingValue) external pure returns (uint256) { return MonetaryFormulas.coverageRatioBps(reserveValue, circulatingValue); } }