Files
bft-crdt-experiment/examples/oracle_consumer.sol

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...
}
}