189 lines
5.8 KiB
Solidity
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);
|
|
}
|
|
}
|
|
|