Initial commit
This commit is contained in:
117
contracts/governance/ConfigRegistry.sol
Normal file
117
contracts/governance/ConfigRegistry.sol
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IConfigRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title ConfigRegistry
|
||||
* @notice Stores all system parameters and limits
|
||||
* @dev Central configuration registry with access control
|
||||
*/
|
||||
contract ConfigRegistry is IConfigRegistry, Ownable {
|
||||
// Constants
|
||||
uint256 private constant HF_SCALE = 1e18;
|
||||
uint256 private constant DEFAULT_MIN_HF = 1.05e18; // 1.05
|
||||
uint256 private constant DEFAULT_TARGET_HF = 1.20e18; // 1.20
|
||||
uint256 private constant DEFAULT_MAX_LOOPS = 5;
|
||||
|
||||
// Core parameters
|
||||
uint256 public override maxLoops = DEFAULT_MAX_LOOPS;
|
||||
uint256 public override minHealthFactor = DEFAULT_MIN_HF;
|
||||
uint256 public override targetHealthFactor = DEFAULT_TARGET_HF;
|
||||
|
||||
// Asset-specific limits
|
||||
mapping(address => uint256) public override maxFlashSize;
|
||||
mapping(address => bool) public override isAllowedAsset;
|
||||
|
||||
// Provider capacity caps
|
||||
mapping(bytes32 => uint256) public override providerCap;
|
||||
|
||||
// Parameter name constants (for events)
|
||||
bytes32 public constant PARAM_MAX_LOOPS = keccak256("MAX_LOOPS");
|
||||
bytes32 public constant PARAM_MIN_HF = keccak256("MIN_HF");
|
||||
bytes32 public constant PARAM_TARGET_HF = keccak256("TARGET_HF");
|
||||
bytes32 public constant PARAM_MAX_FLASH = keccak256("MAX_FLASH");
|
||||
bytes32 public constant PARAM_PROVIDER_CAP = keccak256("PROVIDER_CAP");
|
||||
|
||||
modifier validHealthFactor(uint256 hf) {
|
||||
require(hf >= 1e18, "HF must be >= 1.0");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {
|
||||
// Initialize with safe defaults
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum loops
|
||||
*/
|
||||
function setMaxLoops(uint256 newMaxLoops) external override onlyOwner {
|
||||
require(newMaxLoops > 0 && newMaxLoops <= 50, "Invalid max loops");
|
||||
uint256 oldValue = maxLoops;
|
||||
maxLoops = newMaxLoops;
|
||||
emit ParameterUpdated(PARAM_MAX_LOOPS, oldValue, newMaxLoops);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum flash size for an asset
|
||||
*/
|
||||
function setMaxFlashSize(address asset, uint256 newMaxFlash) external override onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
uint256 oldValue = maxFlashSize[asset];
|
||||
maxFlashSize[asset] = newMaxFlash;
|
||||
emit ParameterUpdated(keccak256(abi.encodePacked(PARAM_MAX_FLASH, asset)), oldValue, newMaxFlash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update minimum health factor
|
||||
*/
|
||||
function setMinHealthFactor(uint256 newMinHF) external override onlyOwner validHealthFactor(newMinHF) {
|
||||
require(newMinHF <= targetHealthFactor, "Min HF must be <= target HF");
|
||||
uint256 oldValue = minHealthFactor;
|
||||
minHealthFactor = newMinHF;
|
||||
emit ParameterUpdated(PARAM_MIN_HF, oldValue, newMinHF);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update target health factor
|
||||
*/
|
||||
function setTargetHealthFactor(uint256 newTargetHF) external override onlyOwner validHealthFactor(newTargetHF) {
|
||||
require(newTargetHF >= minHealthFactor, "Target HF must be >= min HF");
|
||||
uint256 oldValue = targetHealthFactor;
|
||||
targetHealthFactor = newTargetHF;
|
||||
emit ParameterUpdated(PARAM_TARGET_HF, oldValue, newTargetHF);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Add or remove allowed asset
|
||||
*/
|
||||
function setAllowedAsset(address asset, bool allowed) external override onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
bool oldValue = isAllowedAsset[asset];
|
||||
isAllowedAsset[asset] = allowed;
|
||||
emit ParameterUpdated(keccak256(abi.encodePacked("ALLOWED_ASSET", asset)), oldValue ? 1 : 0, allowed ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update provider capacity cap
|
||||
*/
|
||||
function setProviderCap(bytes32 provider, uint256 newCap) external override onlyOwner {
|
||||
require(provider != bytes32(0), "Invalid provider");
|
||||
uint256 oldValue = providerCap[provider];
|
||||
providerCap[provider] = newCap;
|
||||
emit ParameterUpdated(keccak256(abi.encodePacked(PARAM_PROVIDER_CAP, provider)), oldValue, newCap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Batch update allowed assets
|
||||
*/
|
||||
function batchSetAllowedAssets(address[] calldata assets, bool[] calldata allowed) external onlyOwner {
|
||||
require(assets.length == allowed.length, "Array length mismatch");
|
||||
for (uint256 i = 0; i < assets.length; i++) {
|
||||
setAllowedAsset(assets[i], allowed[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
217
contracts/governance/GovernanceGuard.sol
Normal file
217
contracts/governance/GovernanceGuard.sol
Normal file
@@ -0,0 +1,217 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IPolicyEngine.sol";
|
||||
import "../interfaces/IConfigRegistry.sol";
|
||||
import "../interfaces/IVault.sol";
|
||||
|
||||
/**
|
||||
* @title GovernanceGuard
|
||||
* @notice Enforces invariants and policy checks before execution
|
||||
* @dev Acts as the final gatekeeper for all system actions
|
||||
*/
|
||||
contract GovernanceGuard is Ownable {
|
||||
IPolicyEngine public policyEngine;
|
||||
IConfigRegistry public configRegistry;
|
||||
IVault public vault;
|
||||
|
||||
// Strategy throttling
|
||||
struct ThrottleConfig {
|
||||
uint256 dailyCap;
|
||||
uint256 monthlyCap;
|
||||
uint256 dailyCount;
|
||||
uint256 monthlyCount;
|
||||
uint256 lastDailyReset;
|
||||
uint256 lastMonthlyReset;
|
||||
}
|
||||
|
||||
mapping(bytes32 => ThrottleConfig) private strategyThrottles;
|
||||
|
||||
event InvariantCheckFailed(string reason);
|
||||
event PolicyCheckFailed(string reason);
|
||||
event ThrottleExceeded(string strategy, string period);
|
||||
|
||||
modifier onlyVault() {
|
||||
require(msg.sender == address(vault), "Only vault");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
address _policyEngine,
|
||||
address _configRegistry,
|
||||
address _vault,
|
||||
address initialOwner
|
||||
) Ownable(initialOwner) {
|
||||
require(_policyEngine != address(0), "Invalid policy engine");
|
||||
require(_configRegistry != address(0), "Invalid config registry");
|
||||
require(_vault != address(0), "Invalid vault");
|
||||
|
||||
policyEngine = IPolicyEngine(_policyEngine);
|
||||
configRegistry = IConfigRegistry(_configRegistry);
|
||||
vault = IVault(_vault);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify invariants before action
|
||||
* @param actionType Action type identifier
|
||||
* @param actionData Action-specific data
|
||||
* @return success True if all checks pass
|
||||
*/
|
||||
function verifyInvariants(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view returns (bool success) {
|
||||
// Policy check
|
||||
(bool policyAllowed, string memory policyReason) = policyEngine.evaluateAll(actionType, actionData);
|
||||
if (!policyAllowed) {
|
||||
return false; // Would emit event in actual execution
|
||||
}
|
||||
|
||||
// Position invariant check (for amortization actions)
|
||||
if (actionType == keccak256("AMORTIZATION")) {
|
||||
// Decode and verify position improvement
|
||||
// This would decode the expected position changes and verify
|
||||
// For now, return true - actual implementation would check
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check and enforce invariants (with revert)
|
||||
* @param actionType Action type
|
||||
* @param actionData Action data
|
||||
*/
|
||||
function enforceInvariants(bytes32 actionType, bytes memory actionData) external {
|
||||
// Policy check
|
||||
(bool policyAllowed, string memory policyReason) = policyEngine.evaluateAll(actionType, actionData);
|
||||
if (!policyAllowed) {
|
||||
emit PolicyCheckFailed(policyReason);
|
||||
revert(string(abi.encodePacked("Policy check failed: ", policyReason)));
|
||||
}
|
||||
|
||||
// Throttle check
|
||||
if (!checkThrottle(actionType)) {
|
||||
emit ThrottleExceeded(_bytes32ToString(actionType), "daily or monthly");
|
||||
revert("Strategy throttle exceeded");
|
||||
}
|
||||
|
||||
// Record throttle usage
|
||||
recordThrottleUsage(actionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify position improved (invariant check)
|
||||
* @param collateralBefore Previous collateral value
|
||||
* @param debtBefore Previous debt value
|
||||
* @param healthFactorBefore Previous health factor
|
||||
*/
|
||||
function verifyPositionImproved(
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
) external view returns (bool) {
|
||||
return vault.verifyPositionImproved(collateralBefore, debtBefore, healthFactorBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check throttle limits
|
||||
*/
|
||||
function checkThrottle(bytes32 strategy) public view returns (bool) {
|
||||
ThrottleConfig storage throttle = strategyThrottles[strategy];
|
||||
|
||||
// Reset if needed
|
||||
uint256 currentDailyCount = throttle.dailyCount;
|
||||
uint256 currentMonthlyCount = throttle.monthlyCount;
|
||||
|
||||
if (block.timestamp - throttle.lastDailyReset >= 1 days) {
|
||||
currentDailyCount = 0;
|
||||
}
|
||||
if (block.timestamp - throttle.lastMonthlyReset >= 30 days) {
|
||||
currentMonthlyCount = 0;
|
||||
}
|
||||
|
||||
// Check limits
|
||||
if (throttle.dailyCap > 0 && currentDailyCount >= throttle.dailyCap) {
|
||||
return false;
|
||||
}
|
||||
if (throttle.monthlyCap > 0 && currentMonthlyCount >= throttle.monthlyCap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record throttle usage
|
||||
*/
|
||||
function recordThrottleUsage(bytes32 strategy) internal {
|
||||
ThrottleConfig storage throttle = strategyThrottles[strategy];
|
||||
|
||||
// Reset daily if needed
|
||||
if (block.timestamp - throttle.lastDailyReset >= 1 days) {
|
||||
throttle.dailyCount = 0;
|
||||
throttle.lastDailyReset = block.timestamp;
|
||||
}
|
||||
|
||||
// Reset monthly if needed
|
||||
if (block.timestamp - throttle.lastMonthlyReset >= 30 days) {
|
||||
throttle.monthlyCount = 0;
|
||||
throttle.lastMonthlyReset = block.timestamp;
|
||||
}
|
||||
|
||||
throttle.dailyCount++;
|
||||
throttle.monthlyCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Configure throttle for a strategy
|
||||
*/
|
||||
function setThrottle(
|
||||
bytes32 strategy,
|
||||
uint256 dailyCap,
|
||||
uint256 monthlyCap
|
||||
) external onlyOwner {
|
||||
strategyThrottles[strategy] = ThrottleConfig({
|
||||
dailyCap: dailyCap,
|
||||
monthlyCap: monthlyCap,
|
||||
dailyCount: 0,
|
||||
monthlyCount: 0,
|
||||
lastDailyReset: block.timestamp,
|
||||
lastMonthlyReset: block.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update policy engine
|
||||
*/
|
||||
function setPolicyEngine(address newPolicyEngine) external onlyOwner {
|
||||
require(newPolicyEngine != address(0), "Invalid policy engine");
|
||||
policyEngine = IPolicyEngine(newPolicyEngine);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update config registry
|
||||
*/
|
||||
function setConfigRegistry(address newConfigRegistry) external onlyOwner {
|
||||
require(newConfigRegistry != address(0), "Invalid config registry");
|
||||
configRegistry = IConfigRegistry(newConfigRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Helper to convert bytes32 to string
|
||||
*/
|
||||
function _bytes32ToString(bytes32 _bytes32) private pure returns (string memory) {
|
||||
uint8 i = 0;
|
||||
while (i < 32 && _bytes32[i] != 0) {
|
||||
i++;
|
||||
}
|
||||
bytes memory bytesArray = new bytes(i);
|
||||
for (i = 0; i < 32 && _bytes32[i] != 0; i++) {
|
||||
bytesArray[i] = _bytes32[i];
|
||||
}
|
||||
return string(bytesArray);
|
||||
}
|
||||
}
|
||||
|
||||
129
contracts/governance/PolicyEngine.sol
Normal file
129
contracts/governance/PolicyEngine.sol
Normal file
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IPolicyEngine.sol";
|
||||
import "../interfaces/IPolicyModule.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyEngine
|
||||
* @notice Aggregates policy decisions from multiple modules
|
||||
* @dev All registered modules must approve an action for it to be allowed
|
||||
*/
|
||||
contract PolicyEngine is IPolicyEngine, Ownable {
|
||||
// Registered policy modules
|
||||
address[] private policyModules;
|
||||
mapping(address => bool) private isRegisteredModule;
|
||||
|
||||
modifier onlyRegistered(address module) {
|
||||
require(isRegisteredModule[module], "Module not registered");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {}
|
||||
|
||||
/**
|
||||
* @notice Register a policy module
|
||||
*/
|
||||
function registerPolicyModule(address module) external override onlyOwner {
|
||||
require(module != address(0), "Invalid module");
|
||||
require(!isRegisteredModule[module], "Module already registered");
|
||||
|
||||
// Verify it implements IPolicyModule
|
||||
try IPolicyModule(module).name() returns (string memory) {
|
||||
// Module is valid
|
||||
} catch {
|
||||
revert("Invalid policy module");
|
||||
}
|
||||
|
||||
policyModules.push(module);
|
||||
isRegisteredModule[module] = true;
|
||||
|
||||
emit PolicyModuleRegistered(module, IPolicyModule(module).name());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unregister a policy module
|
||||
*/
|
||||
function unregisterPolicyModule(address module) external override onlyOwner onlyRegistered(module) {
|
||||
// Remove from array
|
||||
for (uint256 i = 0; i < policyModules.length; i++) {
|
||||
if (policyModules[i] == module) {
|
||||
policyModules[i] = policyModules[policyModules.length - 1];
|
||||
policyModules.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delete isRegisteredModule[module];
|
||||
emit PolicyModuleUnregistered(module);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate all registered policy modules
|
||||
* @return allowed True if ALL modules allow the action
|
||||
* @return reason Reason from first denying module
|
||||
*/
|
||||
function evaluateAll(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override returns (bool allowed, string memory reason) {
|
||||
// If no modules registered, allow by default
|
||||
if (policyModules.length == 0) {
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
// Check all modules
|
||||
for (uint256 i = 0; i < policyModules.length; i++) {
|
||||
address module = policyModules[i];
|
||||
|
||||
// Skip if module is disabled
|
||||
try IPolicyModule(module).isEnabled() returns (bool enabled) {
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
continue; // Skip if check fails
|
||||
}
|
||||
|
||||
// Get decision from module
|
||||
IPolicyModule.PolicyDecision memory decision;
|
||||
try IPolicyModule(module).evaluate(actionType, actionData) returns (IPolicyModule.PolicyDecision memory d) {
|
||||
decision = d;
|
||||
} catch {
|
||||
// If evaluation fails, deny for safety
|
||||
return (false, "Policy evaluation failed");
|
||||
}
|
||||
|
||||
// If any module denies, return denial
|
||||
if (!decision.allowed) {
|
||||
return (false, decision.reason);
|
||||
}
|
||||
}
|
||||
|
||||
// All modules allowed
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get all registered policy modules
|
||||
*/
|
||||
function getPolicyModules() external view override returns (address[] memory) {
|
||||
return policyModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if a module is registered
|
||||
*/
|
||||
function isRegistered(address module) external view override returns (bool) {
|
||||
return isRegisteredModule[module];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get number of registered modules
|
||||
*/
|
||||
function getModuleCount() external view returns (uint256) {
|
||||
return policyModules.length;
|
||||
}
|
||||
}
|
||||
|
||||
187
contracts/governance/policies/PolicyFlashVolume.sol
Normal file
187
contracts/governance/policies/PolicyFlashVolume.sol
Normal file
@@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyFlashVolume
|
||||
* @notice Policy module that limits flash loan volume per time period
|
||||
* @dev Prevents excessive flash loan usage
|
||||
*/
|
||||
contract PolicyFlashVolume is IPolicyModule, Ownable {
|
||||
string public constant override name = "FlashVolume";
|
||||
|
||||
bool private _enabled = true;
|
||||
|
||||
// Time period for volume tracking (e.g., 1 day = 86400 seconds)
|
||||
uint256 public periodDuration = 1 days;
|
||||
|
||||
// Volume limits per period
|
||||
mapping(address => uint256) public assetVolumeLimit; // Per asset limit
|
||||
uint256 public globalVolumeLimit = type(uint256).max; // Global limit
|
||||
|
||||
// Volume tracking
|
||||
struct VolumePeriod {
|
||||
uint256 volume;
|
||||
uint256 startTime;
|
||||
uint256 endTime;
|
||||
}
|
||||
|
||||
mapping(address => mapping(uint256 => VolumePeriod)) private assetVolumes; // asset => periodId => VolumePeriod
|
||||
mapping(uint256 => VolumePeriod) private globalVolumes; // periodId => VolumePeriod
|
||||
|
||||
event VolumeLimitUpdated(address indexed asset, uint256 oldLimit, uint256 newLimit);
|
||||
event GlobalVolumeLimitUpdated(uint256 oldLimit, uint256 newLimit);
|
||||
event PeriodDurationUpdated(uint256 oldDuration, uint256 newDuration);
|
||||
|
||||
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 (FLASH_LOAN, etc.)
|
||||
* @param actionData Encoded action data: (asset, amount)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
if (actionType != keccak256("FLASH_LOAN")) {
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
(address asset, uint256 amount) = abi.decode(actionData, (address, uint256));
|
||||
|
||||
// Get current period
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
|
||||
// Check asset-specific limit
|
||||
if (assetVolumeLimit[asset] > 0) {
|
||||
VolumePeriod storage assetPeriod = assetVolumes[asset][periodId];
|
||||
uint256 newVolume = assetPeriod.volume + amount;
|
||||
|
||||
if (newVolume > assetVolumeLimit[asset]) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Asset volume limit exceeded"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check global limit
|
||||
if (globalVolumeLimit < type(uint256).max) {
|
||||
VolumePeriod storage globalPeriod = globalVolumes[periodId];
|
||||
uint256 newVolume = globalPeriod.volume + amount;
|
||||
|
||||
if (newVolume > globalVolumeLimit) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Global volume limit exceeded"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record flash loan volume
|
||||
*/
|
||||
function recordVolume(address asset, uint256 amount) external {
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
|
||||
// Update asset volume
|
||||
VolumePeriod storage assetPeriod = assetVolumes[asset][periodId];
|
||||
if (assetPeriod.startTime == 0) {
|
||||
assetPeriod.startTime = block.timestamp;
|
||||
assetPeriod.endTime = block.timestamp + periodDuration;
|
||||
}
|
||||
assetPeriod.volume += amount;
|
||||
|
||||
// Update global volume
|
||||
VolumePeriod storage globalPeriod = globalVolumes[periodId];
|
||||
if (globalPeriod.startTime == 0) {
|
||||
globalPeriod.startTime = block.timestamp;
|
||||
globalPeriod.endTime = block.timestamp + periodDuration;
|
||||
}
|
||||
globalPeriod.volume += amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set volume limit for an asset
|
||||
*/
|
||||
function setAssetVolumeLimit(address asset, uint256 limit) external onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
uint256 oldLimit = assetVolumeLimit[asset];
|
||||
assetVolumeLimit[asset] = limit;
|
||||
emit VolumeLimitUpdated(asset, oldLimit, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set global volume limit
|
||||
*/
|
||||
function setGlobalVolumeLimit(uint256 limit) external onlyOwner {
|
||||
uint256 oldLimit = globalVolumeLimit;
|
||||
globalVolumeLimit = limit;
|
||||
emit GlobalVolumeLimitUpdated(oldLimit, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set period duration
|
||||
*/
|
||||
function setPeriodDuration(uint256 duration) external onlyOwner {
|
||||
require(duration > 0, "Invalid duration");
|
||||
uint256 oldDuration = periodDuration;
|
||||
periodDuration = duration;
|
||||
emit PeriodDurationUpdated(oldDuration, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current period ID
|
||||
*/
|
||||
function getCurrentPeriodId() public view returns (uint256) {
|
||||
return block.timestamp / periodDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current period volume for an asset
|
||||
*/
|
||||
function getAssetPeriodVolume(address asset) external view returns (uint256) {
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
return assetVolumes[asset][periodId].volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current period global volume
|
||||
*/
|
||||
function getGlobalPeriodVolume() external view returns (uint256) {
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
return globalVolumes[periodId].volume;
|
||||
}
|
||||
}
|
||||
|
||||
188
contracts/governance/policies/PolicyHFTrend.sol
Normal file
188
contracts/governance/policies/PolicyHFTrend.sol
Normal file
@@ -0,0 +1,188 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
141
contracts/governance/policies/PolicyLiquiditySpread.sol
Normal file
141
contracts/governance/policies/PolicyLiquiditySpread.sol
Normal file
@@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyLiquiditySpread
|
||||
* @notice Policy module that validates liquidity spreads
|
||||
* @dev Ensures sufficient liquidity depth for operations
|
||||
*/
|
||||
contract PolicyLiquiditySpread is IPolicyModule, Ownable {
|
||||
string public constant override name = "LiquiditySpread";
|
||||
|
||||
bool private _enabled = true;
|
||||
|
||||
// Maximum acceptable spread (basis points, e.g., 50 = 0.5%)
|
||||
uint256 public maxSpreadBps = 50; // 0.5%
|
||||
uint256 private constant BPS_SCALE = 10000;
|
||||
|
||||
// Minimum liquidity depth required (in USD, scaled by 1e8)
|
||||
mapping(address => uint256) public minLiquidityDepth;
|
||||
|
||||
// Interface for checking liquidity
|
||||
interface ILiquidityChecker {
|
||||
function getLiquidityDepth(address asset) external view returns (uint256);
|
||||
function getSpread(address asset, uint256 amount) external view returns (uint256);
|
||||
}
|
||||
|
||||
ILiquidityChecker public liquidityChecker;
|
||||
|
||||
event MaxSpreadUpdated(uint256 oldSpread, uint256 newSpread);
|
||||
event MinLiquidityDepthUpdated(address indexed asset, uint256 oldDepth, uint256 newDepth);
|
||||
event LiquidityCheckerUpdated(address oldChecker, address newChecker);
|
||||
|
||||
modifier onlyEnabled() {
|
||||
require(_enabled, "Policy disabled");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner, address _liquidityChecker) Ownable(initialOwner) {
|
||||
liquidityChecker = ILiquidityChecker(_liquidityChecker);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 (SWAP, FLASH_LOAN, etc.)
|
||||
* @param actionData Encoded action data: (asset, amount, spreadBps, liquidityDepth)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
// For swaps and flash loans, check liquidity
|
||||
if (actionType != keccak256("SWAP") && actionType != keccak256("FLASH_LOAN")) {
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
(address asset, uint256 amount, uint256 spreadBps, uint256 liquidityDepth) =
|
||||
abi.decode(actionData, (address, uint256, uint256, uint256));
|
||||
|
||||
// Check spread
|
||||
if (spreadBps > maxSpreadBps) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Spread too high"
|
||||
});
|
||||
}
|
||||
|
||||
// Check minimum liquidity depth
|
||||
uint256 requiredDepth = minLiquidityDepth[asset];
|
||||
if (requiredDepth > 0 && liquidityDepth < requiredDepth) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Insufficient liquidity depth"
|
||||
});
|
||||
}
|
||||
|
||||
// Additional check: ensure liquidity depth is sufficient for the amount
|
||||
// Rule: liquidity should be at least 2x the operation amount
|
||||
if (liquidityDepth < amount * 2) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Liquidity depth insufficient for operation size"
|
||||
});
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum spread
|
||||
*/
|
||||
function setMaxSpread(uint256 newSpreadBps) external onlyOwner {
|
||||
require(newSpreadBps <= BPS_SCALE, "Invalid spread");
|
||||
uint256 oldSpread = maxSpreadBps;
|
||||
maxSpreadBps = newSpreadBps;
|
||||
emit MaxSpreadUpdated(oldSpread, newSpreadBps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update minimum liquidity depth for an asset
|
||||
*/
|
||||
function setMinLiquidityDepth(address asset, uint256 depth) external onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
uint256 oldDepth = minLiquidityDepth[asset];
|
||||
minLiquidityDepth[asset] = depth;
|
||||
emit MinLiquidityDepthUpdated(asset, oldDepth, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update liquidity checker contract
|
||||
*/
|
||||
function setLiquidityChecker(address newChecker) external onlyOwner {
|
||||
require(newChecker != address(0), "Invalid checker");
|
||||
address oldChecker = address(liquidityChecker);
|
||||
liquidityChecker = ILiquidityChecker(newChecker);
|
||||
emit LiquidityCheckerUpdated(oldChecker, newChecker);
|
||||
}
|
||||
}
|
||||
|
||||
200
contracts/governance/policies/PolicyProviderConcentration.sol
Normal file
200
contracts/governance/policies/PolicyProviderConcentration.sol
Normal file
@@ -0,0 +1,200 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../../interfaces/IFlashLoanRouter.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyProviderConcentration
|
||||
* @notice Policy module that prevents over-concentration in single providers
|
||||
* @dev Ensures diversification across flash loan providers
|
||||
*/
|
||||
contract PolicyProviderConcentration is IPolicyModule, Ownable {
|
||||
string public constant override name = "ProviderConcentration";
|
||||
|
||||
bool private _enabled = true;
|
||||
|
||||
// Maximum percentage of total flash loans from a single provider (basis points)
|
||||
uint256 public maxProviderConcentrationBps = 5000; // 50%
|
||||
uint256 private constant BPS_SCALE = 10000;
|
||||
|
||||
// Time window for concentration tracking
|
||||
uint256 public trackingWindow = 7 days;
|
||||
|
||||
// Provider usage tracking
|
||||
struct ProviderUsage {
|
||||
uint256 totalVolume;
|
||||
uint256 lastResetTime;
|
||||
mapping(IFlashLoanRouter.FlashLoanProvider => uint256) providerVolumes;
|
||||
}
|
||||
|
||||
mapping(address => ProviderUsage) private vaultProviderUsage; // vault => ProviderUsage
|
||||
ProviderUsage private globalProviderUsage;
|
||||
|
||||
event MaxConcentrationUpdated(uint256 oldMax, uint256 newMax);
|
||||
event TrackingWindowUpdated(uint256 oldWindow, uint256 newWindow);
|
||||
|
||||
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 (FLASH_LOAN, etc.)
|
||||
* @param actionData Encoded action data: (vault, asset, amount, provider)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
if (actionType != keccak256("FLASH_LOAN")) {
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
(
|
||||
address vault,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
IFlashLoanRouter.FlashLoanProvider provider
|
||||
) = abi.decode(actionData, (address, address, uint256, IFlashLoanRouter.FlashLoanProvider));
|
||||
|
||||
// Reset usage if window expired
|
||||
ProviderUsage storage vaultUsage = vaultProviderUsage[vault];
|
||||
if (block.timestamp - vaultUsage.lastResetTime > trackingWindow) {
|
||||
// Would reset in actual implementation, but for evaluation assume fresh window
|
||||
vaultUsage = globalProviderUsage; // Use global as proxy for "reset" state
|
||||
}
|
||||
|
||||
// Calculate new provider volume
|
||||
uint256 newProviderVolume = vaultUsage.providerVolumes[provider] + amount;
|
||||
uint256 newTotalVolume = vaultUsage.totalVolume + amount;
|
||||
|
||||
if (newTotalVolume > 0) {
|
||||
uint256 newConcentration = (newProviderVolume * BPS_SCALE) / newTotalVolume;
|
||||
|
||||
if (newConcentration > maxProviderConcentrationBps) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Provider concentration limit exceeded"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record flash loan usage
|
||||
*/
|
||||
function recordUsage(
|
||||
address vault,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
IFlashLoanRouter.FlashLoanProvider provider
|
||||
) external {
|
||||
// Reset if window expired
|
||||
ProviderUsage storage vaultUsage = vaultProviderUsage[vault];
|
||||
if (block.timestamp - vaultUsage.lastResetTime > trackingWindow) {
|
||||
_resetUsage(vault);
|
||||
vaultUsage = vaultProviderUsage[vault];
|
||||
}
|
||||
|
||||
// Update usage
|
||||
vaultUsage.providerVolumes[provider] += amount;
|
||||
vaultUsage.totalVolume += amount;
|
||||
|
||||
// Update global usage
|
||||
ProviderUsage storage global = globalProviderUsage;
|
||||
if (block.timestamp - global.lastResetTime > trackingWindow) {
|
||||
_resetGlobalUsage();
|
||||
global = globalProviderUsage;
|
||||
}
|
||||
global.providerVolumes[provider] += amount;
|
||||
global.totalVolume += amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Reset usage for a vault
|
||||
*/
|
||||
function _resetUsage(address vault) internal {
|
||||
ProviderUsage storage usage = vaultProviderUsage[vault];
|
||||
usage.totalVolume = 0;
|
||||
usage.lastResetTime = block.timestamp;
|
||||
|
||||
// Reset all provider volumes
|
||||
for (uint256 i = 0; i <= uint256(IFlashLoanRouter.FlashLoanProvider.DAI_FLASH); i++) {
|
||||
usage.providerVolumes[IFlashLoanRouter.FlashLoanProvider(i)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Reset global usage
|
||||
*/
|
||||
function _resetGlobalUsage() internal {
|
||||
globalProviderUsage.totalVolume = 0;
|
||||
globalProviderUsage.lastResetTime = block.timestamp;
|
||||
|
||||
for (uint256 i = 0; i <= uint256(IFlashLoanRouter.FlashLoanProvider.DAI_FLASH); i++) {
|
||||
globalProviderUsage.providerVolumes[IFlashLoanRouter.FlashLoanProvider(i)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum provider concentration
|
||||
*/
|
||||
function setMaxConcentration(uint256 newMaxBps) external onlyOwner {
|
||||
require(newMaxBps <= BPS_SCALE, "Invalid concentration");
|
||||
uint256 oldMax = maxProviderConcentrationBps;
|
||||
maxProviderConcentrationBps = newMaxBps;
|
||||
emit MaxConcentrationUpdated(oldMax, newMaxBps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update tracking window
|
||||
*/
|
||||
function setTrackingWindow(uint256 newWindow) external onlyOwner {
|
||||
require(newWindow > 0, "Invalid window");
|
||||
uint256 oldWindow = trackingWindow;
|
||||
trackingWindow = newWindow;
|
||||
emit TrackingWindowUpdated(oldWindow, newWindow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get provider concentration for a vault
|
||||
*/
|
||||
function getProviderConcentration(
|
||||
address vault,
|
||||
IFlashLoanRouter.FlashLoanProvider provider
|
||||
) external view returns (uint256 concentrationBps) {
|
||||
ProviderUsage storage usage = vaultProviderUsage[vault];
|
||||
if (usage.totalVolume == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (usage.providerVolumes[provider] * BPS_SCALE) / usage.totalVolume;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user