101 lines
3.8 KiB
Solidity
101 lines
3.8 KiB
Solidity
// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||
import "./interfaces/IChainlinkPriceFeed.sol";
|
||
|
||
/// @title ExchangeRateOracle — 汇率预言机集成
|
||
/// @notice 集成 Chainlink Price Feed,提供各币种对 USD 汇率
|
||
/// @dev 支持多币种,内置过期保护(最多15分钟)
|
||
contract ExchangeRateOracle is Initializable, AccessControlUpgradeable {
|
||
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
|
||
|
||
// 币种 → Chainlink Price Feed 地址
|
||
mapping(string => address) public priceFeeds;
|
||
// 支持的币种列表
|
||
string[] public supportedCurrencies;
|
||
|
||
// 汇率数据最大容许延迟(秒)
|
||
uint256 public maxStaleness;
|
||
|
||
// --- Events ---
|
||
event PriceFeedSet(string indexed currency, address feed);
|
||
event PriceFeedRemoved(string indexed currency);
|
||
event MaxStalenessUpdated(uint256 oldValue, uint256 newValue);
|
||
|
||
function initialize(uint256 _maxStaleness, address admin) external initializer {
|
||
__AccessControl_init();
|
||
|
||
maxStaleness = _maxStaleness > 0 ? _maxStaleness : 900; // 默认 15 分钟
|
||
|
||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||
_grantRole(ADMIN_ROLE, admin);
|
||
}
|
||
|
||
/// @notice 获取某币种对 USD 的汇率
|
||
/// @param currency 币种标识(如 "CNY", "JPY", "EUR")
|
||
/// @return rate 汇率(Chainlink 精度,通常 8 位小数)
|
||
function getRate(string calldata currency) external view returns (uint256 rate) {
|
||
address feed = priceFeeds[currency];
|
||
require(feed != address(0), "Oracle: unsupported currency");
|
||
|
||
(, int256 answer,, uint256 updatedAt,) = IChainlinkPriceFeed(feed).latestRoundData();
|
||
require(answer > 0, "Oracle: invalid price");
|
||
require(block.timestamp - updatedAt <= maxStaleness, "Oracle: stale price data");
|
||
|
||
rate = uint256(answer);
|
||
}
|
||
|
||
/// @notice 获取汇率及详细信息
|
||
function getRateDetails(string calldata currency)
|
||
external
|
||
view
|
||
returns (uint256 rate, uint256 updatedAt, uint8 decimals)
|
||
{
|
||
address feed = priceFeeds[currency];
|
||
require(feed != address(0), "Oracle: unsupported currency");
|
||
|
||
(, int256 answer,, uint256 _updatedAt,) = IChainlinkPriceFeed(feed).latestRoundData();
|
||
require(answer > 0, "Oracle: invalid price");
|
||
|
||
rate = uint256(answer);
|
||
updatedAt = _updatedAt;
|
||
decimals = IChainlinkPriceFeed(feed).decimals();
|
||
}
|
||
|
||
/// @notice 设置币种的 Price Feed 地址
|
||
function setPriceFeed(string calldata currency, address feed) external onlyRole(ADMIN_ROLE) {
|
||
require(feed != address(0), "Oracle: zero address");
|
||
require(bytes(currency).length > 0, "Oracle: empty currency");
|
||
|
||
// 新增还是更新
|
||
if (priceFeeds[currency] == address(0)) {
|
||
supportedCurrencies.push(currency);
|
||
}
|
||
|
||
priceFeeds[currency] = feed;
|
||
emit PriceFeedSet(currency, feed);
|
||
}
|
||
|
||
/// @notice 移除币种 Price Feed
|
||
function removePriceFeed(string calldata currency) external onlyRole(ADMIN_ROLE) {
|
||
require(priceFeeds[currency] != address(0), "Oracle: feed not found");
|
||
priceFeeds[currency] = address(0);
|
||
emit PriceFeedRemoved(currency);
|
||
}
|
||
|
||
/// @notice 更新最大过期时间
|
||
function setMaxStaleness(uint256 _maxStaleness) external onlyRole(ADMIN_ROLE) {
|
||
require(_maxStaleness > 0, "Oracle: zero staleness");
|
||
uint256 old = maxStaleness;
|
||
maxStaleness = _maxStaleness;
|
||
emit MaxStalenessUpdated(old, _maxStaleness);
|
||
}
|
||
|
||
/// @notice 获取支持的币种数量
|
||
function supportedCurrencyCount() external view returns (uint256) {
|
||
return supportedCurrencies.length;
|
||
}
|
||
}
|