267 lines
9.0 KiB
Solidity
267 lines
9.0 KiB
Solidity
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
pragma solidity ^0.8.24;
|
||
|
|
|
||
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||
|
|
import "../interfaces/IOracleAdapter.sol";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title DBISOracleAdapter
|
||
|
|
* @notice Standardizes pricing from multiple oracle sources
|
||
|
|
* @dev Aggregates prices from Aave, Chainlink, Uniswap TWAP with confidence scoring
|
||
|
|
*/
|
||
|
|
contract DBISOracleAdapter is IOracleAdapter, Ownable {
|
||
|
|
// Constants
|
||
|
|
uint256 private constant PRICE_SCALE = 1e8;
|
||
|
|
uint256 private constant CONFIDENCE_SCALE = 1e18;
|
||
|
|
uint256 private constant MAX_PRICE_AGE = 1 hours;
|
||
|
|
|
||
|
|
// Chainlink price feed interface (simplified)
|
||
|
|
interface AggregatorV3Interface {
|
||
|
|
function latestRoundData()
|
||
|
|
external
|
||
|
|
view
|
||
|
|
returns (
|
||
|
|
uint80 roundId,
|
||
|
|
int256 answer,
|
||
|
|
uint256 startedAt,
|
||
|
|
uint256 updatedAt,
|
||
|
|
uint80 answeredInRound
|
||
|
|
);
|
||
|
|
|
||
|
|
function decimals() external view returns (uint8);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Aave Oracle interface (simplified)
|
||
|
|
interface IAaveOracle {
|
||
|
|
function getAssetPrice(address asset) external view returns (uint256);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Asset configuration
|
||
|
|
struct AssetConfig {
|
||
|
|
address chainlinkFeed;
|
||
|
|
address aaveOracle;
|
||
|
|
address uniswapPool;
|
||
|
|
bool enabled;
|
||
|
|
uint256 chainlinkWeight; // Weight for price aggregation (out of 1e18)
|
||
|
|
uint256 aaveWeight;
|
||
|
|
uint256 uniswapWeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
mapping(address => AssetConfig) public assetConfigs;
|
||
|
|
address public immutable aaveOracleAddress;
|
||
|
|
|
||
|
|
// Price cache with TTL
|
||
|
|
struct CachedPrice {
|
||
|
|
uint256 price;
|
||
|
|
uint256 timestamp;
|
||
|
|
OracleSource source;
|
||
|
|
}
|
||
|
|
|
||
|
|
mapping(address => CachedPrice) private priceCache;
|
||
|
|
|
||
|
|
event AssetConfigUpdated(address indexed asset, address chainlinkFeed, address uniswapPool);
|
||
|
|
event PriceCacheUpdated(address indexed asset, uint256 price, OracleSource source);
|
||
|
|
|
||
|
|
constructor(address _aaveOracleAddress, address initialOwner) Ownable(initialOwner) {
|
||
|
|
require(_aaveOracleAddress != address(0), "Invalid Aave Oracle");
|
||
|
|
aaveOracleAddress = _aaveOracleAddress;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Configure an asset's oracle sources
|
||
|
|
*/
|
||
|
|
function configureAsset(
|
||
|
|
address asset,
|
||
|
|
address chainlinkFeed,
|
||
|
|
address uniswapPool,
|
||
|
|
uint256 chainlinkWeight,
|
||
|
|
uint256 aaveWeight,
|
||
|
|
uint256 uniswapWeight
|
||
|
|
) external onlyOwner {
|
||
|
|
require(asset != address(0), "Invalid asset");
|
||
|
|
require(chainlinkWeight + aaveWeight + uniswapWeight == CONFIDENCE_SCALE, "Weights must sum to 1e18");
|
||
|
|
|
||
|
|
assetConfigs[asset] = AssetConfig({
|
||
|
|
chainlinkFeed: chainlinkFeed,
|
||
|
|
aaveOracle: aaveOracleAddress,
|
||
|
|
uniswapPool: uniswapPool,
|
||
|
|
enabled: true,
|
||
|
|
chainlinkWeight: chainlinkWeight,
|
||
|
|
aaveWeight: aaveWeight,
|
||
|
|
uniswapWeight: uniswapWeight
|
||
|
|
});
|
||
|
|
|
||
|
|
emit AssetConfigUpdated(asset, chainlinkFeed, uniswapPool);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Enable or disable an asset
|
||
|
|
*/
|
||
|
|
function setAssetEnabled(address asset, bool enabled) external onlyOwner {
|
||
|
|
require(assetConfigs[asset].chainlinkFeed != address(0) ||
|
||
|
|
assetConfigs[asset].aaveOracle != address(0), "Asset not configured");
|
||
|
|
assetConfigs[asset].enabled = enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get latest price for an asset
|
||
|
|
*/
|
||
|
|
function getPrice(address asset) external view override returns (PriceData memory) {
|
||
|
|
return getPriceWithMaxAge(asset, MAX_PRICE_AGE);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get latest price with max age requirement
|
||
|
|
*/
|
||
|
|
function getPriceWithMaxAge(
|
||
|
|
address asset,
|
||
|
|
uint256 maxAge
|
||
|
|
) public view override returns (PriceData memory) {
|
||
|
|
AssetConfig memory config = assetConfigs[asset];
|
||
|
|
require(config.enabled, "Asset not enabled");
|
||
|
|
|
||
|
|
// Try cached price first if fresh
|
||
|
|
CachedPrice memory cached = priceCache[asset];
|
||
|
|
if (cached.timestamp > 0 && block.timestamp - cached.timestamp <= maxAge) {
|
||
|
|
return PriceData({
|
||
|
|
price: cached.price,
|
||
|
|
source: cached.source,
|
||
|
|
timestamp: cached.timestamp,
|
||
|
|
confidence: CONFIDENCE_SCALE / 2 // Moderate confidence for cache
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get prices from available sources
|
||
|
|
uint256 chainlinkPrice = 0;
|
||
|
|
uint256 aavePrice = 0;
|
||
|
|
uint256 uniswapPrice = 0;
|
||
|
|
uint256 chainlinkTime = 0;
|
||
|
|
uint256 aaveTime = block.timestamp;
|
||
|
|
uint256 uniswapTime = 0;
|
||
|
|
|
||
|
|
// Chainlink
|
||
|
|
if (config.chainlinkFeed != address(0)) {
|
||
|
|
try AggregatorV3Interface(config.chainlinkFeed).latestRoundData() returns (
|
||
|
|
uint80,
|
||
|
|
int256 answer,
|
||
|
|
uint256,
|
||
|
|
uint256 updatedAt,
|
||
|
|
uint80
|
||
|
|
) {
|
||
|
|
if (answer > 0 && block.timestamp - updatedAt <= maxAge) {
|
||
|
|
uint8 decimals = AggregatorV3Interface(config.chainlinkFeed).decimals();
|
||
|
|
chainlinkPrice = uint256(answer);
|
||
|
|
chainlinkTime = updatedAt;
|
||
|
|
|
||
|
|
// Normalize to 8 decimals
|
||
|
|
if (decimals > 8) {
|
||
|
|
chainlinkPrice = chainlinkPrice / (10 ** (decimals - 8));
|
||
|
|
} else if (decimals < 8) {
|
||
|
|
chainlinkPrice = chainlinkPrice * (10 ** (8 - decimals));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Aave Oracle
|
||
|
|
if (config.aaveOracle != address(0)) {
|
||
|
|
try IAaveOracle(config.aaveOracle).getAssetPrice(asset) returns (uint256 price) {
|
||
|
|
if (price > 0) {
|
||
|
|
aavePrice = price;
|
||
|
|
}
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Uniswap TWAP (simplified - would need actual TWAP implementation)
|
||
|
|
// For now, return fallback
|
||
|
|
if (config.uniswapPool != address(0)) {
|
||
|
|
// Placeholder - would integrate with Uniswap V3 TWAP oracle
|
||
|
|
uniswapTime = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Aggregate prices using weights
|
||
|
|
uint256 totalWeight = 0;
|
||
|
|
uint256 weightedPrice = 0;
|
||
|
|
OracleSource source = OracleSource.FALLBACK;
|
||
|
|
uint256 latestTimestamp = 0;
|
||
|
|
|
||
|
|
if (chainlinkPrice > 0 && config.chainlinkWeight > 0) {
|
||
|
|
weightedPrice += chainlinkPrice * config.chainlinkWeight;
|
||
|
|
totalWeight += config.chainlinkWeight;
|
||
|
|
if (chainlinkTime > latestTimestamp) {
|
||
|
|
latestTimestamp = chainlinkTime;
|
||
|
|
source = OracleSource.CHAINLINK;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (aavePrice > 0 && config.aaveWeight > 0) {
|
||
|
|
weightedPrice += aavePrice * config.aaveWeight;
|
||
|
|
totalWeight += config.aaveWeight;
|
||
|
|
if (aaveTime > latestTimestamp) {
|
||
|
|
latestTimestamp = aaveTime;
|
||
|
|
source = OracleSource.AAVE;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (uniswapPrice > 0 && config.uniswapWeight > 0) {
|
||
|
|
weightedPrice += uniswapPrice * config.uniswapWeight;
|
||
|
|
totalWeight += config.uniswapWeight;
|
||
|
|
if (uniswapTime > latestTimestamp) {
|
||
|
|
latestTimestamp = uniswapTime;
|
||
|
|
source = OracleSource.UNISWAP_TWAP;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
require(totalWeight > 0, "No valid price source");
|
||
|
|
uint256 aggregatedPrice = weightedPrice / totalWeight;
|
||
|
|
uint256 confidence = (totalWeight * CONFIDENCE_SCALE) / (config.chainlinkWeight + config.aaveWeight + config.uniswapWeight);
|
||
|
|
|
||
|
|
return PriceData({
|
||
|
|
price: aggregatedPrice,
|
||
|
|
source: source,
|
||
|
|
timestamp: latestTimestamp > 0 ? latestTimestamp : block.timestamp,
|
||
|
|
confidence: confidence
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get aggregated price from multiple sources
|
||
|
|
*/
|
||
|
|
function getAggregatedPrice(address asset) external view override returns (uint256 price, uint256 confidence) {
|
||
|
|
PriceData memory priceData = getPrice(asset);
|
||
|
|
return (priceData.price, priceData.confidence);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Convert amount from one asset to another
|
||
|
|
*/
|
||
|
|
function convertAmount(
|
||
|
|
address fromAsset,
|
||
|
|
uint256 fromAmount,
|
||
|
|
address toAsset
|
||
|
|
) external view override returns (uint256) {
|
||
|
|
PriceData memory fromPrice = getPrice(fromAsset);
|
||
|
|
PriceData memory toPrice = getPrice(toAsset);
|
||
|
|
|
||
|
|
require(fromPrice.price > 0 && toPrice.price > 0, "Invalid prices");
|
||
|
|
|
||
|
|
// Both prices are in USD with 8 decimals
|
||
|
|
// Convert: fromAmount * fromPrice / toPrice
|
||
|
|
return (fromAmount * fromPrice.price) / toPrice.price;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Update price cache (called by keeper or internal)
|
||
|
|
*/
|
||
|
|
function updatePriceCache(address asset) external {
|
||
|
|
PriceData memory priceData = getPriceWithMaxAge(asset, MAX_PRICE_AGE);
|
||
|
|
priceCache[asset] = CachedPrice({
|
||
|
|
price: priceData.price,
|
||
|
|
timestamp: block.timestamp,
|
||
|
|
source: priceData.source
|
||
|
|
});
|
||
|
|
emit PriceCacheUpdated(asset, priceData.price, priceData.source);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|