// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "../../interfaces/IYTAssetVault.sol"; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; contract YTPriceFeed is Initializable, UUPSUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } error Forbidden(); error MaxChangeTooHigh(); error PriceChangeTooLarge(); error SpreadTooHigh(); error InvalidAddress(); error InvalidChainlinkPrice(); address public gov; uint256 public constant PRICE_PRECISION = 10 ** 30; uint256 public constant BASIS_POINTS_DIVISOR = 10000; uint256 public constant MAX_SPREAD_BASIS_POINTS = 200; address public usdcAddress; uint256 public maxPriceChangeBps; AggregatorV3Interface internal usdcPriceFeed; mapping(address => uint256) public spreadBasisPoints; mapping(address => uint256) public lastPrice; mapping(address => bool) public isKeeper; event PriceUpdate(address indexed token, uint256 oldPrice, uint256 newPrice, uint256 timestamp); event SpreadUpdate(address indexed token, uint256 spreadBps); event KeeperSet(address indexed keeper, bool isActive); modifier onlyGov() { if (msg.sender != gov) revert Forbidden(); _; } modifier onlyKeeper() { if (!isKeeper[msg.sender] && msg.sender != gov) revert Forbidden(); _; } function initialize(address _usdcAddress, address _usdcPriceFeed) external initializer { __UUPSUpgradeable_init(); if (_usdcAddress == address(0)) revert InvalidAddress(); usdcAddress = _usdcAddress; usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed); gov = msg.sender; maxPriceChangeBps = 500; } function setUSDCAddress(address _usdcAddress) external onlyGov { if (_usdcAddress == address(0)) revert InvalidAddress(); usdcAddress = _usdcAddress; } function setUSDCPriceFeed(address _usdcPriceFeed) external onlyGov { usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed); } function _authorizeUpgrade(address newImplementation) internal override onlyGov {} function setKeeper(address _keeper, bool _isActive) external onlyGov { isKeeper[_keeper] = _isActive; emit KeeperSet(_keeper, _isActive); } function setMaxPriceChangeBps(uint256 _maxPriceChangeBps) external onlyGov { if (_maxPriceChangeBps > 2000) revert MaxChangeTooHigh(); maxPriceChangeBps = _maxPriceChangeBps; } function setSpreadBasisPoints(address _token, uint256 _spreadBasisPoints) external onlyGov { if (_spreadBasisPoints > MAX_SPREAD_BASIS_POINTS) revert SpreadTooHigh(); spreadBasisPoints[_token] = _spreadBasisPoints; emit SpreadUpdate(_token, _spreadBasisPoints); } function setSpreadBasisPointsForMultiple( address[] calldata _tokens, uint256[] calldata _spreadBasisPoints ) external onlyGov { require(_tokens.length == _spreadBasisPoints.length, "length mismatch"); for (uint256 i = 0; i < _tokens.length; i++) { if (_spreadBasisPoints[i] > MAX_SPREAD_BASIS_POINTS) revert SpreadTooHigh(); spreadBasisPoints[_tokens[i]] = _spreadBasisPoints[i]; emit SpreadUpdate(_tokens[i], _spreadBasisPoints[i]); } } function updatePrice(address _token) external onlyKeeper returns (uint256) { if (_token == usdcAddress) { return _getUSDCPrice(); } uint256 oldPrice = lastPrice[_token]; uint256 newPrice = _getRawPrice(_token); _validatePriceChange(_token, newPrice); lastPrice[_token] = newPrice; emit PriceUpdate(_token, oldPrice, newPrice, block.timestamp); return newPrice; } function forceUpdatePrice(address _token, uint256 _price) external onlyGov { uint256 oldPrice = lastPrice[_token]; lastPrice[_token] = _price; emit PriceUpdate(_token, oldPrice, _price, block.timestamp); } function getPrice(address _token, bool _maximise) external view returns (uint256) { if (_token == usdcAddress) { return _getUSDCPrice(); } uint256 basePrice = _getRawPrice(_token); _validatePriceChange(_token, basePrice); return _applySpread(_token, basePrice, _maximise); } function _getRawPrice(address _token) private view returns (uint256) { return IYTAssetVault(_token).ytPrice(); } function _getUSDCPrice() internal view returns (uint256) { ( /* uint80 roundId */, int256 price, /* uint256 startedAt */, /* uint256 updatedAt */, /* uint80 answeredInRound */ ) = usdcPriceFeed.latestRoundData(); if (price <= 0) revert InvalidChainlinkPrice(); return uint256(price) * 1e22; // 1e22 = 10^(30-8) } function _applySpread( address _token, uint256 _basePrice, bool _maximise ) private view returns (uint256) { uint256 spread = spreadBasisPoints[_token]; if (spread == 0) { return _basePrice; } if (_maximise) { return _basePrice * (BASIS_POINTS_DIVISOR + spread) / BASIS_POINTS_DIVISOR; } else { return _basePrice * (BASIS_POINTS_DIVISOR - spread) / BASIS_POINTS_DIVISOR; } } function _validatePriceChange(address _token, uint256 _newPrice) private view { uint256 oldPrice = lastPrice[_token]; if (oldPrice == 0) { return; } uint256 priceDiff = _newPrice > oldPrice ? _newPrice - oldPrice : oldPrice - _newPrice; uint256 maxDiff = oldPrice * maxPriceChangeBps / BASIS_POINTS_DIVISOR; if (priceDiff > maxDiff) revert PriceChangeTooLarge(); } function getPriceInfo(address _token) external view returns ( uint256 currentPrice, uint256 cachedPrice, uint256 maxPrice, uint256 minPrice, uint256 spread ) { if (_token == usdcAddress) { uint256 usdcPrice = _getUSDCPrice(); currentPrice = usdcPrice; cachedPrice = usdcPrice; maxPrice = usdcPrice; minPrice = usdcPrice; spread = 0; } else { currentPrice = _getRawPrice(_token); cachedPrice = lastPrice[_token]; spread = spreadBasisPoints[_token]; maxPrice = _applySpread(_token, currentPrice, true); minPrice = _applySpread(_token, currentPrice, false); } } function getMaxPrice(address _token) external view returns (uint256) { if (_token == usdcAddress) { return _getUSDCPrice(); } uint256 basePrice = _getRawPrice(_token); _validatePriceChange(_token, basePrice); return _applySpread(_token, basePrice, true); } function getMinPrice(address _token) external view returns (uint256) { if (_token == usdcAddress) { return _getUSDCPrice(); } uint256 basePrice = _getRawPrice(_token); _validatePriceChange(_token, basePrice); return _applySpread(_token, basePrice, false); } uint256[50] private __gap; }