Files
no_five/contracts/governance/policies/PolicyHFTrend.sol
2025-11-20 15:35:25 -08:00

189 lines
5.8 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "../IPolicyModule.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title PolicyHFTrend
* @notice Policy module that monitors health factor trends
* @dev Prevents actions that would worsen health factor trajectory
*/
contract PolicyHFTrend is IPolicyModule, Ownable {
string public constant override name = "HealthFactorTrend";
bool private _enabled = true;
uint256 private constant HF_SCALE = 1e18;
uint256 private minHFImprovement = 0.01e18; // 1% minimum improvement
uint256 private minHFThreshold = 1.05e18; // 1.05 minimum HF
// Track HF history for trend analysis
struct HFHistory {
uint256[] values;
uint256[] timestamps;
uint256 maxHistoryLength;
}
mapping(address => HFHistory) private vaultHistory;
event HFThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
event MinHFImprovementUpdated(uint256 oldMin, uint256 newMin);
modifier onlyEnabled() {
require(_enabled, "Policy disabled");
_;
}
constructor(address initialOwner) Ownable(initialOwner) {}
/**
* @notice Check if module is enabled
*/
function isEnabled() external view override returns (bool) {
return _enabled;
}
/**
* @notice Enable or disable the module
*/
function setEnabled(bool enabled) external override onlyOwner {
_enabled = enabled;
}
/**
* @notice Evaluate policy for proposed action
* @param actionType Action type (AMORTIZATION, LEVERAGE, etc.)
* @param actionData Encoded action data: (vault, hfBefore, hfAfter, collateralChange, debtChange)
*/
function evaluate(
bytes32 actionType,
bytes memory actionData
) external view override onlyEnabled returns (PolicyDecision memory) {
// Decode action data
(
address vault,
uint256 hfBefore,
uint256 hfAfter,
int256 collateralChange,
int256 debtChange
) = abi.decode(actionData, (address, uint256, uint256, int256, int256));
// Check minimum HF threshold
if (hfAfter < minHFThreshold) {
return PolicyDecision({
allowed: false,
reason: "HF below minimum threshold"
});
}
// For amortization actions, require improvement
if (actionType == keccak256("AMORTIZATION")) {
if (hfAfter <= hfBefore) {
return PolicyDecision({
allowed: false,
reason: "HF must improve"
});
}
uint256 hfImprovement = hfAfter > hfBefore ? hfAfter - hfBefore : 0;
if (hfImprovement < minHFImprovement) {
return PolicyDecision({
allowed: false,
reason: "HF improvement too small"
});
}
}
// Check trend (require improving trajectory)
if (hfAfter < hfBefore) {
return PolicyDecision({
allowed: false,
reason: "HF trend declining"
});
}
// Check that collateral increases or debt decreases (amortization requirement)
if (actionType == keccak256("AMORTIZATION")) {
if (collateralChange <= 0 && debtChange >= 0) {
return PolicyDecision({
allowed: false,
reason: "Amortization requires collateral increase or debt decrease"
});
}
}
return PolicyDecision({
allowed: true,
reason: ""
});
}
/**
* @notice Update minimum HF threshold
*/
function setMinHFThreshold(uint256 newThreshold) external onlyOwner {
require(newThreshold >= 1e18, "HF must be >= 1.0");
uint256 oldThreshold = minHFThreshold;
minHFThreshold = newThreshold;
emit HFThresholdUpdated(oldThreshold, newThreshold);
}
/**
* @notice Update minimum HF improvement required
*/
function setMinHFImprovement(uint256 newMinImprovement) external onlyOwner {
require(newMinImprovement <= HF_SCALE, "Invalid improvement");
uint256 oldMin = minHFImprovement;
minHFImprovement = newMinImprovement;
emit MinHFImprovementUpdated(oldMin, newMinImprovement);
}
/**
* @notice Get minimum HF threshold
*/
function getMinHFThreshold() external view returns (uint256) {
return minHFThreshold;
}
/**
* @notice Record HF value for trend tracking
*/
function recordHF(address vault, uint256 hf) external {
HFHistory storage history = vaultHistory[vault];
if (history.maxHistoryLength == 0) {
history.maxHistoryLength = 10; // Default max history
}
history.values.push(hf);
history.timestamps.push(block.timestamp);
// Limit history length
if (history.values.length > history.maxHistoryLength) {
// Remove oldest entry (shift array)
for (uint256 i = 0; i < history.values.length - 1; i++) {
history.values[i] = history.values[i + 1];
history.timestamps[i] = history.timestamps[i + 1];
}
history.values.pop();
history.timestamps.pop();
}
}
/**
* @notice Get HF trend (slope)
* @return trend Positive = improving, negative = declining
*/
function getHFTrend(address vault) external view returns (int256 trend) {
HFHistory storage history = vaultHistory[vault];
if (history.values.length < 2) {
return 0;
}
uint256 latest = history.values[history.values.length - 1];
uint256 previous = history.values[history.values.length - 2];
return int256(latest) - int256(previous);
}
}