// 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; } }