// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./OraclePriceFeed.sol"; /** * @title PriceFeedKeeper * @notice Keeper contract for automated price feed updates * @dev Can be called by external keepers (Chainlink Keepers, Gelato, etc.) to update price feeds */ contract PriceFeedKeeper is AccessControl, ReentrancyGuard { bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); bytes32 public constant UPKEEPER_ROLE = keccak256("UPKEEPER_ROLE"); // Can update keeper addresses OraclePriceFeed public oraclePriceFeed; // Asset tracking address[] public trackedAssets; mapping(address => bool) public isTracked; // Update configuration (default 6s aligns with PMM mesh automation; admin may set via setUpdateInterval) uint256 public updateInterval = 6; // seconds mapping(address => uint256) public lastUpdateTime; // Keeper configuration uint256 public maxUpdatesPerCall = 10; // Maximum assets to update per call uint256 public gasBuffer = 50000; // Gas buffer for keeper operations event AssetTracked(address indexed asset); event AssetUntracked(address indexed asset); event PriceFeedsUpdated(address[] assets, uint256 timestamp); event UpdateIntervalChanged(uint256 oldInterval, uint256 newInterval); event MaxUpdatesPerCallChanged(uint256 oldMax, uint256 newMax); constructor(address admin, address oraclePriceFeed_) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(KEEPER_ROLE, admin); _grantRole(UPKEEPER_ROLE, admin); oraclePriceFeed = OraclePriceFeed(oraclePriceFeed_); } /** * @notice Add asset to tracking list * @param asset Address of the asset to track */ function trackAsset(address asset) external onlyRole(UPKEEPER_ROLE) { require(asset != address(0), "PriceFeedKeeper: zero address"); require(!isTracked[asset], "PriceFeedKeeper: already tracked"); trackedAssets.push(asset); isTracked[asset] = true; emit AssetTracked(asset); } /** * @notice Remove asset from tracking list * @param asset Address of the asset to untrack */ function untrackAsset(address asset) external onlyRole(UPKEEPER_ROLE) { require(isTracked[asset], "PriceFeedKeeper: not tracked"); // Remove from array for (uint256 i = 0; i < trackedAssets.length; i++) { if (trackedAssets[i] == asset) { trackedAssets[i] = trackedAssets[trackedAssets.length - 1]; trackedAssets.pop(); break; } } isTracked[asset] = false; delete lastUpdateTime[asset]; emit AssetUntracked(asset); } /** * @notice Check if any assets need updating * @return updateNeeded True if any assets need updating * @return assets Array of assets that need updating */ function checkUpkeep() public view returns (bool updateNeeded, address[] memory assets) { address[] memory needsUpdateList = new address[](trackedAssets.length); uint256 count = 0; for (uint256 i = 0; i < trackedAssets.length; i++) { address asset = trackedAssets[i]; if (_needsUpdate(asset)) { needsUpdateList[count] = asset; count++; } } if (count > 0) { // Resize array assets = new address[](count); for (uint256 i = 0; i < count; i++) { assets[i] = needsUpdateList[i]; } updateNeeded = true; } else { assets = new address[](0); updateNeeded = false; } } /** * @notice Perform upkeep - update price feeds that need updating * @return success True if updates were successful * @return updatedAssets Array of assets that were updated */ function performUpkeep() external onlyRole(KEEPER_ROLE) nonReentrant returns ( bool success, address[] memory updatedAssets ) { address[] memory needsUpdateList = new address[](trackedAssets.length); uint256 count = 0; // Collect assets that need updating for (uint256 i = 0; i < trackedAssets.length; i++) { address asset = trackedAssets[i]; if (_needsUpdate(asset) && count < maxUpdatesPerCall) { needsUpdateList[count] = asset; count++; } } if (count == 0) { return (true, new address[](0)); } // Resize array updatedAssets = new address[](count); for (uint256 i = 0; i < count; i++) { updatedAssets[i] = needsUpdateList[i]; } // Update price feeds try oraclePriceFeed.updateMultiplePriceFeeds(updatedAssets) { // Update last update time uint256 currentTime = block.timestamp; for (uint256 i = 0; i < count; i++) { lastUpdateTime[updatedAssets[i]] = currentTime; } emit PriceFeedsUpdated(updatedAssets, currentTime); success = true; } catch { success = false; } } /** * @notice Update specific assets * @param assets Array of asset addresses to update */ function updateAssets(address[] calldata assets) external onlyRole(KEEPER_ROLE) nonReentrant { require(assets.length > 0, "PriceFeedKeeper: empty array"); require(assets.length <= maxUpdatesPerCall, "PriceFeedKeeper: too many assets"); // Verify all assets are tracked for (uint256 i = 0; i < assets.length; i++) { require(isTracked[assets[i]], "PriceFeedKeeper: asset not tracked"); } oraclePriceFeed.updateMultiplePriceFeeds(assets); uint256 currentTime = block.timestamp; for (uint256 i = 0; i < assets.length; i++) { lastUpdateTime[assets[i]] = currentTime; } emit PriceFeedsUpdated(assets, currentTime); } /** * @notice Check if a specific asset needs updating * @param asset Address of the asset * @return needsUpdate True if asset needs updating */ function needsUpdate(address asset) external view returns (bool) { return _needsUpdate(asset); } /** * @notice Get all tracked assets * @return assets Array of tracked asset addresses */ function getTrackedAssets() external view returns (address[] memory) { return trackedAssets; } /** * @notice Set update interval * @param interval New update interval in seconds */ function setUpdateInterval(uint256 interval) external onlyRole(DEFAULT_ADMIN_ROLE) { require(interval > 0, "PriceFeedKeeper: zero interval"); uint256 oldInterval = updateInterval; updateInterval = interval; emit UpdateIntervalChanged(oldInterval, interval); } /** * @notice Set maximum updates per call * @param max New maximum updates per call */ function setMaxUpdatesPerCall(uint256 max) external onlyRole(DEFAULT_ADMIN_ROLE) { require(max > 0, "PriceFeedKeeper: zero max"); uint256 oldMax = maxUpdatesPerCall; maxUpdatesPerCall = max; emit MaxUpdatesPerCallChanged(oldMax, max); } /** * @notice Set oracle price feed address * @param oraclePriceFeed_ New oracle price feed address */ function setOraclePriceFeed(address oraclePriceFeed_) external onlyRole(DEFAULT_ADMIN_ROLE) { require(oraclePriceFeed_ != address(0), "PriceFeedKeeper: zero address"); oraclePriceFeed = OraclePriceFeed(oraclePriceFeed_); } /** * @notice Internal function to check if asset needs update * @param asset Address of the asset * @return True if asset needs updating */ function _needsUpdate(address asset) internal view returns (bool) { if (!isTracked[asset]) { return false; } uint256 lastUpdate = lastUpdateTime[asset]; if (lastUpdate == 0) { return true; // Never updated } return block.timestamp - lastUpdate >= updateInterval; } /** * @notice Get gas estimate for upkeep * @return gasEstimate Estimated gas needed for upkeep */ function getUpkeepGasEstimate() external view returns (uint256 gasEstimate) { (bool needsUpdate_, address[] memory assets) = checkUpkeep(); if (!needsUpdate_ || assets.length == 0) { return 0; } // Base gas + gas per asset update gasEstimate = 50000 + (assets.length * 30000) + gasBuffer; } }