106 lines
3.9 KiB
Solidity
106 lines
3.9 KiB
Solidity
// 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);
|
|
}
|
|
}
|