2025-12-18 13:07:35 +08:00
|
|
|
// 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";
|
2025-12-24 16:41:26 +08:00
|
|
|
import "../../interfaces/IYTAssetVault.sol";
|
|
|
|
|
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
contract YTPriceFeed is Initializable, UUPSUpgradeable {
|
|
|
|
|
|
2025-12-23 14:05:41 +08:00
|
|
|
/// @custom:oz-upgrades-unsafe-allow constructor
|
|
|
|
|
constructor() {
|
|
|
|
|
_disableInitializers();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 13:07:35 +08:00
|
|
|
error Forbidden();
|
|
|
|
|
error MaxChangeTooHigh();
|
|
|
|
|
error PriceChangeTooLarge();
|
|
|
|
|
error SpreadTooHigh();
|
|
|
|
|
error InvalidAddress();
|
2025-12-24 16:41:26 +08:00
|
|
|
error InvalidChainlinkPrice();
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
address public gov;
|
|
|
|
|
|
|
|
|
|
uint256 public constant PRICE_PRECISION = 10 ** 30;
|
|
|
|
|
uint256 public constant BASIS_POINTS_DIVISOR = 10000;
|
2025-12-26 13:38:10 +08:00
|
|
|
uint256 public constant MAX_SPREAD_BASIS_POINTS = 200;
|
2025-12-18 13:07:35 +08:00
|
|
|
|
2025-12-24 16:41:26 +08:00
|
|
|
address public usdcAddress;
|
2025-12-18 13:07:35 +08:00
|
|
|
|
2025-12-26 13:38:10 +08:00
|
|
|
uint256 public maxPriceChangeBps;
|
2025-12-24 16:41:26 +08:00
|
|
|
|
|
|
|
|
AggregatorV3Interface internal usdcPriceFeed;
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
_;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 16:41:26 +08:00
|
|
|
function initialize(address _usdcAddress, address _usdcPriceFeed) external initializer {
|
2025-12-18 13:07:35 +08:00
|
|
|
__UUPSUpgradeable_init();
|
2025-12-24 16:41:26 +08:00
|
|
|
if (_usdcAddress == address(0)) revert InvalidAddress();
|
|
|
|
|
usdcAddress = _usdcAddress;
|
|
|
|
|
usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed);
|
2025-12-18 13:07:35 +08:00
|
|
|
gov = msg.sender;
|
2025-12-26 13:38:10 +08:00
|
|
|
maxPriceChangeBps = 500;
|
2025-12-18 13:07:35 +08:00
|
|
|
}
|
2025-12-24 16:41:26 +08:00
|
|
|
|
|
|
|
|
function setUSDCAddress(address _usdcAddress) external onlyGov {
|
|
|
|
|
if (_usdcAddress == address(0)) revert InvalidAddress();
|
|
|
|
|
usdcAddress = _usdcAddress;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setUSDCPriceFeed(address _usdcPriceFeed) external onlyGov {
|
|
|
|
|
usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed);
|
|
|
|
|
}
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
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 {
|
2025-12-26 13:38:10 +08:00
|
|
|
if (_maxPriceChangeBps > 2000) revert MaxChangeTooHigh();
|
2025-12-18 13:07:35 +08:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-24 16:41:26 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
function forceUpdatePrice(address _token, uint256 _price) external onlyGov {
|
|
|
|
|
uint256 oldPrice = lastPrice[_token];
|
|
|
|
|
lastPrice[_token] = _price;
|
|
|
|
|
emit PriceUpdate(_token, oldPrice, _price, block.timestamp);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 13:29:35 +08:00
|
|
|
|
2025-12-18 13:07:35 +08:00
|
|
|
function getPrice(address _token, bool _maximise) external view returns (uint256) {
|
2025-12-24 16:41:26 +08:00
|
|
|
if (_token == usdcAddress) {
|
|
|
|
|
return _getUSDCPrice();
|
2025-12-18 13:07:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint256 basePrice = _getRawPrice(_token);
|
|
|
|
|
|
|
|
|
|
_validatePriceChange(_token, basePrice);
|
|
|
|
|
|
|
|
|
|
return _applySpread(_token, basePrice, _maximise);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _getRawPrice(address _token) private view returns (uint256) {
|
2025-12-24 16:41:26 +08:00
|
|
|
return IYTAssetVault(_token).ytPrice();
|
2025-12-18 13:07:35 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 16:41:26 +08:00
|
|
|
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)
|
2025-12-18 13:07:35 +08:00
|
|
|
}
|
2025-12-24 16:41:26 +08:00
|
|
|
|
2025-12-18 13:07:35 +08:00
|
|
|
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
|
|
|
|
|
) {
|
2025-12-24 16:41:26 +08:00
|
|
|
if (_token == usdcAddress) {
|
|
|
|
|
uint256 usdcPrice = _getUSDCPrice();
|
|
|
|
|
currentPrice = usdcPrice;
|
|
|
|
|
cachedPrice = usdcPrice;
|
|
|
|
|
maxPrice = usdcPrice;
|
|
|
|
|
minPrice = usdcPrice;
|
2025-12-18 13:07:35 +08:00
|
|
|
spread = 0;
|
|
|
|
|
} else {
|
|
|
|
|
currentPrice = _getRawPrice(_token);
|
|
|
|
|
cachedPrice = lastPrice[_token];
|
|
|
|
|
spread = spreadBasisPoints[_token];
|
|
|
|
|
maxPrice = _applySpread(_token, currentPrice, true);
|
|
|
|
|
minPrice = _applySpread(_token, currentPrice, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-25 13:29:35 +08:00
|
|
|
|
2025-12-18 13:07:35 +08:00
|
|
|
function getMaxPrice(address _token) external view returns (uint256) {
|
2025-12-24 16:41:26 +08:00
|
|
|
if (_token == usdcAddress) {
|
|
|
|
|
return _getUSDCPrice();
|
2025-12-18 13:07:35 +08:00
|
|
|
}
|
|
|
|
|
uint256 basePrice = _getRawPrice(_token);
|
|
|
|
|
_validatePriceChange(_token, basePrice);
|
|
|
|
|
return _applySpread(_token, basePrice, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getMinPrice(address _token) external view returns (uint256) {
|
2025-12-24 16:41:26 +08:00
|
|
|
if (_token == usdcAddress) {
|
|
|
|
|
return _getUSDCPrice();
|
2025-12-18 13:07:35 +08:00
|
|
|
}
|
|
|
|
|
uint256 basePrice = _getRawPrice(_token);
|
|
|
|
|
_validatePriceChange(_token, basePrice);
|
|
|
|
|
return _applySpread(_token, basePrice, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint256[50] private __gap;
|
2025-12-25 13:29:35 +08:00
|
|
|
}
|