440 lines
13 KiB
Solidity
440 lines
13 KiB
Solidity
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
pragma solidity ^0.8.19;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title OracleConsumer
|
||
|
|
* @notice Example smart contracts showing how to consume data from the BFT-CRDT oracle network
|
||
|
|
* @dev This demonstrates various aggregation strategies and use cases
|
||
|
|
*/
|
||
|
|
|
||
|
|
interface IOracleNetwork {
|
||
|
|
struct PriceData {
|
||
|
|
uint128 price;
|
||
|
|
uint8 confidence;
|
||
|
|
address oracle;
|
||
|
|
uint256 timestamp;
|
||
|
|
bytes32 sourceHash;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPrices(
|
||
|
|
string calldata assetPair,
|
||
|
|
uint256 startTime,
|
||
|
|
uint256 endTime
|
||
|
|
) external view returns (PriceData[] memory);
|
||
|
|
|
||
|
|
function getOracleReputation(address oracle) external view returns (
|
||
|
|
uint256 qualityScore,
|
||
|
|
uint256 totalAttestations,
|
||
|
|
uint256 anomalyReports
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title PriceAggregator
|
||
|
|
* @notice Advanced price aggregation with multiple strategies
|
||
|
|
*/
|
||
|
|
contract PriceAggregator {
|
||
|
|
IOracleNetwork public immutable oracleNetwork;
|
||
|
|
|
||
|
|
uint256 public constant MIN_SOURCES = 3;
|
||
|
|
uint256 public constant OUTLIER_THRESHOLD = 500; // 5%
|
||
|
|
uint256 public constant CONFIDENCE_THRESHOLD = 80;
|
||
|
|
|
||
|
|
struct AggregatedPrice {
|
||
|
|
uint128 price;
|
||
|
|
uint8 confidence;
|
||
|
|
uint256 numSources;
|
||
|
|
uint256 timestamp;
|
||
|
|
}
|
||
|
|
|
||
|
|
mapping(string => AggregatedPrice) public latestPrices;
|
||
|
|
|
||
|
|
event PriceUpdated(
|
||
|
|
string indexed assetPair,
|
||
|
|
uint128 price,
|
||
|
|
uint8 confidence,
|
||
|
|
uint256 sources
|
||
|
|
);
|
||
|
|
|
||
|
|
constructor(address _oracleNetwork) {
|
||
|
|
oracleNetwork = IOracleNetwork(_oracleNetwork);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get aggregated price using weighted median
|
||
|
|
* @param assetPair The asset pair (e.g., "ETH/USD")
|
||
|
|
* @param maxAge Maximum age of price data in seconds
|
||
|
|
*/
|
||
|
|
function getPrice(
|
||
|
|
string calldata assetPair,
|
||
|
|
uint256 maxAge
|
||
|
|
) external view returns (uint128 price, uint8 confidence) {
|
||
|
|
require(maxAge > 0, "Invalid max age");
|
||
|
|
|
||
|
|
// Get all prices from oracle network
|
||
|
|
IOracleNetwork.PriceData[] memory prices = oracleNetwork.getPrices(
|
||
|
|
assetPair,
|
||
|
|
block.timestamp - maxAge,
|
||
|
|
block.timestamp
|
||
|
|
);
|
||
|
|
|
||
|
|
require(prices.length >= MIN_SOURCES, "Insufficient price sources");
|
||
|
|
|
||
|
|
// Calculate weighted median
|
||
|
|
AggregatedPrice memory aggregated = _calculateWeightedMedian(prices);
|
||
|
|
|
||
|
|
return (aggregated.price, aggregated.confidence);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Calculate Time-Weighted Average Price (TWAP)
|
||
|
|
* @param assetPair The asset pair
|
||
|
|
* @param duration Time window in seconds
|
||
|
|
*/
|
||
|
|
function getTWAP(
|
||
|
|
string calldata assetPair,
|
||
|
|
uint256 duration
|
||
|
|
) external view returns (uint128) {
|
||
|
|
IOracleNetwork.PriceData[] memory prices = oracleNetwork.getPrices(
|
||
|
|
assetPair,
|
||
|
|
block.timestamp - duration,
|
||
|
|
block.timestamp
|
||
|
|
);
|
||
|
|
|
||
|
|
require(prices.length > 0, "No price data available");
|
||
|
|
|
||
|
|
uint256 weightedSum = 0;
|
||
|
|
uint256 totalWeight = 0;
|
||
|
|
|
||
|
|
for (uint i = 0; i < prices.length; i++) {
|
||
|
|
// Weight by time and confidence
|
||
|
|
uint256 timeWeight = duration - (block.timestamp - prices[i].timestamp);
|
||
|
|
uint256 confWeight = prices[i].confidence;
|
||
|
|
uint256 weight = timeWeight * confWeight / 100;
|
||
|
|
|
||
|
|
weightedSum += prices[i].price * weight;
|
||
|
|
totalWeight += weight;
|
||
|
|
}
|
||
|
|
|
||
|
|
return uint128(weightedSum / totalWeight);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Get volatility-adjusted price
|
||
|
|
* @dev Uses standard deviation to adjust confidence
|
||
|
|
*/
|
||
|
|
function getVolatilityAdjustedPrice(
|
||
|
|
string calldata assetPair,
|
||
|
|
uint256 maxAge
|
||
|
|
) external view returns (
|
||
|
|
uint128 price,
|
||
|
|
uint8 confidence,
|
||
|
|
uint128 standardDeviation
|
||
|
|
) {
|
||
|
|
IOracleNetwork.PriceData[] memory prices = oracleNetwork.getPrices(
|
||
|
|
assetPair,
|
||
|
|
block.timestamp - maxAge,
|
||
|
|
block.timestamp
|
||
|
|
);
|
||
|
|
|
||
|
|
require(prices.length >= MIN_SOURCES, "Insufficient sources");
|
||
|
|
|
||
|
|
// Remove outliers first
|
||
|
|
uint128[] memory filteredPrices = _removeOutliers(prices);
|
||
|
|
|
||
|
|
// Calculate mean
|
||
|
|
uint256 sum = 0;
|
||
|
|
for (uint i = 0; i < filteredPrices.length; i++) {
|
||
|
|
sum += filteredPrices[i];
|
||
|
|
}
|
||
|
|
uint128 mean = uint128(sum / filteredPrices.length);
|
||
|
|
|
||
|
|
// Calculate standard deviation
|
||
|
|
uint256 variance = 0;
|
||
|
|
for (uint i = 0; i < filteredPrices.length; i++) {
|
||
|
|
int256 diff = int256(uint256(filteredPrices[i])) - int256(uint256(mean));
|
||
|
|
variance += uint256(diff * diff);
|
||
|
|
}
|
||
|
|
variance = variance / filteredPrices.length;
|
||
|
|
standardDeviation = uint128(_sqrt(variance));
|
||
|
|
|
||
|
|
// Adjust confidence based on volatility
|
||
|
|
uint256 volatilityRatio = (standardDeviation * 10000) / mean;
|
||
|
|
if (volatilityRatio < 100) { // < 1%
|
||
|
|
confidence = 99;
|
||
|
|
} else if (volatilityRatio < 500) { // < 5%
|
||
|
|
confidence = 90;
|
||
|
|
} else if (volatilityRatio < 1000) { // < 10%
|
||
|
|
confidence = 70;
|
||
|
|
} else {
|
||
|
|
confidence = 50;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (mean, confidence, standardDeviation);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Update stored price if newer data is available
|
||
|
|
*/
|
||
|
|
function updatePrice(string calldata assetPair) external {
|
||
|
|
AggregatedPrice memory current = latestPrices[assetPair];
|
||
|
|
|
||
|
|
IOracleNetwork.PriceData[] memory prices = oracleNetwork.getPrices(
|
||
|
|
assetPair,
|
||
|
|
current.timestamp,
|
||
|
|
block.timestamp
|
||
|
|
);
|
||
|
|
|
||
|
|
if (prices.length >= MIN_SOURCES) {
|
||
|
|
AggregatedPrice memory newPrice = _calculateWeightedMedian(prices);
|
||
|
|
latestPrices[assetPair] = newPrice;
|
||
|
|
|
||
|
|
emit PriceUpdated(
|
||
|
|
assetPair,
|
||
|
|
newPrice.price,
|
||
|
|
newPrice.confidence,
|
||
|
|
newPrice.numSources
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function _calculateWeightedMedian(
|
||
|
|
IOracleNetwork.PriceData[] memory prices
|
||
|
|
) private view returns (AggregatedPrice memory) {
|
||
|
|
// Sort prices and calculate weights
|
||
|
|
uint256 length = prices.length;
|
||
|
|
uint128[] memory sortedPrices = new uint128[](length);
|
||
|
|
uint256[] memory weights = new uint256[](length);
|
||
|
|
|
||
|
|
for (uint i = 0; i < length; i++) {
|
||
|
|
sortedPrices[i] = prices[i].price;
|
||
|
|
|
||
|
|
// Calculate weight based on oracle reputation and confidence
|
||
|
|
(uint256 qualityScore,,) = oracleNetwork.getOracleReputation(prices[i].oracle);
|
||
|
|
weights[i] = prices[i].confidence * qualityScore / 100;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bubble sort (gas inefficient but simple for example)
|
||
|
|
for (uint i = 0; i < length - 1; i++) {
|
||
|
|
for (uint j = 0; j < length - i - 1; j++) {
|
||
|
|
if (sortedPrices[j] > sortedPrices[j + 1]) {
|
||
|
|
// Swap prices
|
||
|
|
uint128 tempPrice = sortedPrices[j];
|
||
|
|
sortedPrices[j] = sortedPrices[j + 1];
|
||
|
|
sortedPrices[j + 1] = tempPrice;
|
||
|
|
// Swap weights
|
||
|
|
uint256 tempWeight = weights[j];
|
||
|
|
weights[j] = weights[j + 1];
|
||
|
|
weights[j + 1] = tempWeight;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find weighted median
|
||
|
|
uint256 totalWeight = 0;
|
||
|
|
for (uint i = 0; i < length; i++) {
|
||
|
|
totalWeight += weights[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
uint256 targetWeight = totalWeight / 2;
|
||
|
|
uint256 cumulativeWeight = 0;
|
||
|
|
uint128 medianPrice = sortedPrices[length / 2]; // fallback
|
||
|
|
|
||
|
|
for (uint i = 0; i < length; i++) {
|
||
|
|
cumulativeWeight += weights[i];
|
||
|
|
if (cumulativeWeight >= targetWeight) {
|
||
|
|
medianPrice = sortedPrices[i];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate confidence
|
||
|
|
uint8 avgConfidence = 0;
|
||
|
|
for (uint i = 0; i < length; i++) {
|
||
|
|
avgConfidence += prices[i].confidence;
|
||
|
|
}
|
||
|
|
avgConfidence = avgConfidence / uint8(length);
|
||
|
|
|
||
|
|
return AggregatedPrice({
|
||
|
|
price: medianPrice,
|
||
|
|
confidence: avgConfidence,
|
||
|
|
numSources: length,
|
||
|
|
timestamp: block.timestamp
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function _removeOutliers(
|
||
|
|
IOracleNetwork.PriceData[] memory prices
|
||
|
|
) private pure returns (uint128[] memory) {
|
||
|
|
if (prices.length < 4) {
|
||
|
|
// Not enough data for outlier detection
|
||
|
|
uint128[] memory result = new uint128[](prices.length);
|
||
|
|
for (uint i = 0; i < prices.length; i++) {
|
||
|
|
result[i] = prices[i].price;
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate mean
|
||
|
|
uint256 sum = 0;
|
||
|
|
for (uint i = 0; i < prices.length; i++) {
|
||
|
|
sum += prices[i].price;
|
||
|
|
}
|
||
|
|
uint256 mean = sum / prices.length;
|
||
|
|
|
||
|
|
// Count non-outliers
|
||
|
|
uint256 validCount = 0;
|
||
|
|
for (uint i = 0; i < prices.length; i++) {
|
||
|
|
uint256 deviation = prices[i].price > mean
|
||
|
|
? prices[i].price - mean
|
||
|
|
: mean - prices[i].price;
|
||
|
|
uint256 percentDeviation = (deviation * 10000) / mean;
|
||
|
|
|
||
|
|
if (percentDeviation <= OUTLIER_THRESHOLD) {
|
||
|
|
validCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create filtered array
|
||
|
|
uint128[] memory filtered = new uint128[](validCount);
|
||
|
|
uint256 index = 0;
|
||
|
|
|
||
|
|
for (uint i = 0; i < prices.length; i++) {
|
||
|
|
uint256 deviation = prices[i].price > mean
|
||
|
|
? prices[i].price - mean
|
||
|
|
: mean - prices[i].price;
|
||
|
|
uint256 percentDeviation = (deviation * 10000) / mean;
|
||
|
|
|
||
|
|
if (percentDeviation <= OUTLIER_THRESHOLD) {
|
||
|
|
filtered[index++] = prices[i].price;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return filtered;
|
||
|
|
}
|
||
|
|
|
||
|
|
function _sqrt(uint256 x) private pure returns (uint256) {
|
||
|
|
if (x == 0) return 0;
|
||
|
|
uint256 z = (x + 1) / 2;
|
||
|
|
uint256 y = x;
|
||
|
|
while (z < y) {
|
||
|
|
y = z;
|
||
|
|
z = (x / z + z) / 2;
|
||
|
|
}
|
||
|
|
return y;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title DeFiLendingProtocol
|
||
|
|
* @notice Example lending protocol using BFT-CRDT oracle
|
||
|
|
*/
|
||
|
|
contract DeFiLendingProtocol {
|
||
|
|
PriceAggregator public immutable priceAggregator;
|
||
|
|
|
||
|
|
uint256 public constant COLLATERAL_FACTOR = 8000; // 80%
|
||
|
|
uint256 public constant LIQUIDATION_THRESHOLD = 8500; // 85%
|
||
|
|
uint256 public constant PRICE_STALENESS = 300; // 5 minutes
|
||
|
|
|
||
|
|
mapping(address => mapping(string => uint256)) public deposits;
|
||
|
|
mapping(address => mapping(string => uint256)) public borrows;
|
||
|
|
|
||
|
|
constructor(address _priceAggregator) {
|
||
|
|
priceAggregator = PriceAggregator(_priceAggregator);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Calculate USD value using oracle prices
|
||
|
|
*/
|
||
|
|
function getCollateralValueUSD(
|
||
|
|
address user,
|
||
|
|
string calldata asset
|
||
|
|
) public view returns (uint256) {
|
||
|
|
uint256 amount = deposits[user][asset];
|
||
|
|
if (amount == 0) return 0;
|
||
|
|
|
||
|
|
(uint128 price, uint8 confidence) = priceAggregator.getPrice(
|
||
|
|
string(abi.encodePacked(asset, "/USD")),
|
||
|
|
PRICE_STALENESS
|
||
|
|
);
|
||
|
|
|
||
|
|
require(confidence >= 80, "Price confidence too low");
|
||
|
|
|
||
|
|
// Apply conservative estimate for collateral
|
||
|
|
return (amount * price * COLLATERAL_FACTOR) / 10000 / 1e6;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Check if a position is healthy
|
||
|
|
*/
|
||
|
|
function isPositionHealthy(address user) public view returns (bool) {
|
||
|
|
uint256 totalCollateralUSD = 0;
|
||
|
|
uint256 totalBorrowUSD = 0;
|
||
|
|
|
||
|
|
// In real implementation, would iterate through all assets
|
||
|
|
totalCollateralUSD += getCollateralValueUSD(user, "ETH");
|
||
|
|
totalCollateralUSD += getCollateralValueUSD(user, "BTC");
|
||
|
|
|
||
|
|
// Calculate total borrows in USD
|
||
|
|
uint256 usdcBorrow = borrows[user]["USDC"];
|
||
|
|
totalBorrowUSD += usdcBorrow; // USDC is 1:1 with USD
|
||
|
|
|
||
|
|
if (totalBorrowUSD == 0) return true;
|
||
|
|
|
||
|
|
uint256 healthFactor = (totalCollateralUSD * 10000) / totalBorrowUSD;
|
||
|
|
return healthFactor >= LIQUIDATION_THRESHOLD;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title OptionsProtocol
|
||
|
|
* @notice Example options protocol using oracle for settlement
|
||
|
|
*/
|
||
|
|
contract OptionsProtocol {
|
||
|
|
PriceAggregator public immutable priceAggregator;
|
||
|
|
|
||
|
|
struct Option {
|
||
|
|
string assetPair;
|
||
|
|
uint128 strikePrice;
|
||
|
|
uint256 expiry;
|
||
|
|
bool isCall;
|
||
|
|
bool isSettled;
|
||
|
|
uint128 settlementPrice;
|
||
|
|
}
|
||
|
|
|
||
|
|
mapping(uint256 => Option) public options;
|
||
|
|
uint256 public nextOptionId;
|
||
|
|
|
||
|
|
constructor(address _priceAggregator) {
|
||
|
|
priceAggregator = PriceAggregator(_priceAggregator);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Settle an option at expiry using TWAP
|
||
|
|
*/
|
||
|
|
function settleOption(uint256 optionId) external {
|
||
|
|
Option storage option = options[optionId];
|
||
|
|
require(!option.isSettled, "Already settled");
|
||
|
|
require(block.timestamp >= option.expiry, "Not expired");
|
||
|
|
|
||
|
|
// Use 1-hour TWAP around expiry for fair settlement
|
||
|
|
uint128 settlementPrice = priceAggregator.getTWAP(
|
||
|
|
option.assetPair,
|
||
|
|
3600 // 1 hour
|
||
|
|
);
|
||
|
|
|
||
|
|
option.settlementPrice = settlementPrice;
|
||
|
|
option.isSettled = true;
|
||
|
|
|
||
|
|
// Calculate payoff
|
||
|
|
uint256 payoff = 0;
|
||
|
|
if (option.isCall && settlementPrice > option.strikePrice) {
|
||
|
|
payoff = settlementPrice - option.strikePrice;
|
||
|
|
} else if (!option.isCall && settlementPrice < option.strikePrice) {
|
||
|
|
payoff = option.strikePrice - settlementPrice;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process payoff...
|
||
|
|
}
|
||
|
|
}
|