Created
March 24, 2025 09:35
-
-
Save mayorcoded/938d5f681c29baf2feed5c2a5e7463a0 to your computer and use it in GitHub Desktop.
RAAVE Data Stream Integrations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity 0.8.19; | |
| import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol"; | |
| import {StreamsLookupCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/interfaces/StreamsLookupCompatibleInterface.sol"; | |
| import {ILogAutomation, Log} from "@chainlink/contracts/src/v0.8/automation/interfaces/ILogAutomation.sol"; | |
| import {IRewardManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IRewardManager.sol"; | |
| import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IVerifierFeeManager.sol"; | |
| import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC20.sol"; | |
| /** | |
| * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES. | |
| * DO NOT USE THIS CODE IN PRODUCTION. | |
| */ | |
| // Custom interfaces for IVerifierProxy and IFeeManager | |
| interface IVerifierProxy { | |
| /** | |
| * @notice Verifies that the data encoded has been signed. | |
| * correctly by routing to the correct verifier, and bills the user if applicable. | |
| * @param payload The encoded data to be verified, including the signed | |
| * report. | |
| * @param parameterPayload Fee metadata for billing. For the current implementation this is just the abi-encoded fee token ERC-20 address. | |
| * @return verifierResponse The encoded report from the verifier. | |
| */ | |
| function verify( | |
| bytes calldata payload, | |
| bytes calldata parameterPayload | |
| ) external payable returns (bytes memory verifierResponse); | |
| function s_feeManager() external view returns (IVerifierFeeManager); | |
| } | |
| interface IFeeManager { | |
| /** | |
| * @notice Calculates the fee and reward associated with verifying a report, including discounts for subscribers. | |
| * This function assesses the fee and reward for report verification, applying a discount for recognized subscriber addresses. | |
| * @param subscriber The address attempting to verify the report. A discount is applied if this address | |
| * is recognized as a subscriber. | |
| * @param unverifiedReport The report data awaiting verification. The content of this report is used to | |
| * determine the base fee and reward, before considering subscriber discounts. | |
| * @param quoteAddress The payment token address used for quoting fees and rewards. | |
| * @return fee The fee assessed for verifying the report, with subscriber discounts applied where applicable. | |
| * @return reward The reward allocated to the caller for successfully verifying the report. | |
| * @return totalDiscount The total discount amount deducted from the fee for subscribers | |
| */ | |
| function getFeeAndReward( | |
| address subscriber, | |
| bytes memory unverifiedReport, | |
| address quoteAddress | |
| ) external returns (Common.Asset memory, Common.Asset memory, uint256); | |
| function i_linkAddress() external view returns (address); | |
| function i_nativeAddress() external view returns (address); | |
| function i_rewardManager() external view returns (address); | |
| } | |
| contract PriceOracleStreamsUpkeep is ILogAutomation, StreamsLookupCompatibleInterface, IPriceOracle { | |
| error InvalidReportVersion(uint16 version); // Thrown when an unsupported report version is provided to verifyReport. | |
| error AssetNotRegistered(address asset); // Thrown when trying to get price for an unregistered asset | |
| error UnauthorizedAccess(); // Thrown when unauthorized access is attempted | |
| error InvalidFeedId(string feedId); // Thrown when an invalid feed ID is provided | |
| /** | |
| * @dev Represents a data report from a Data Streams stream for v3 schema (crypto streams). | |
| * The `price`, `bid`, and `ask` values are carried to either 8 or 18 decimal places, depending on the stream. | |
| * For more information, see https://docs.chain.link/data-streams/crypto-streams and https://docs.chain.link/data-streams/reference/report-schema | |
| */ | |
| struct ReportV3 { | |
| bytes32 feedId; // The stream ID the report has data for. | |
| uint32 validFromTimestamp; // Earliest timestamp for which price is applicable. | |
| uint32 observationsTimestamp; // Latest timestamp for which price is applicable. | |
| uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain's native token (e.g., WETH/ETH). | |
| uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK. | |
| uint32 expiresAt; // Latest timestamp where the report can be verified onchain. | |
| int192 price; // DON consensus median price (8 or 18 decimals). | |
| int192 bid; // Simulated price impact of a buy order up to the X% depth of liquidity utilisation (8 or 18 decimals). | |
| int192 ask; // Simulated price impact of a sell order up to the X% depth of liquidity utilisation (8 or 18 decimals). | |
| } | |
| struct PriceData { | |
| uint256 price; // Normalized price | |
| uint8 decimals; // Decimals for this feed | |
| uint256 timestamp; // Last update timestamp | |
| } | |
| struct Quote { | |
| address quoteAddress; | |
| } | |
| IVerifierProxy public verifier; | |
| address public owner; | |
| string public constant DATASTREAMS_FEEDLABEL = "feedIDs"; | |
| string public constant DATASTREAMS_QUERYLABEL = "timestamp"; | |
| // Maps feedId (as string) to PriceData | |
| mapping(string => PriceData) public feedPriceData; | |
| // Maps asset address to its feedId | |
| mapping(address => string) public assetToFeedId; | |
| // Array of feedIds that this contract subscribes to | |
| string[] public feedIds; | |
| event AssetFeedAdded(address indexed asset, string feedId, uint8 decimals); | |
| event PriceUpdated(string indexed feedId, uint256 price, uint256 timestamp); | |
| modifier onlyOwner() { | |
| if (msg.sender != owner) revert UnauthorizedAccess(); | |
| _; | |
| } | |
| constructor(address _verifier) { | |
| verifier = IVerifierProxy(_verifier); | |
| owner = msg.sender; | |
| } | |
| /** | |
| * @notice Add a new asset feed to the oracle | |
| * @param feedId The feed ID for the asset | |
| * @param asset The address of the asset | |
| * @param decimals The number of decimals for the asset price | |
| */ | |
| function addAssetFeed(string calldata feedId, address asset, uint8 decimals) external onlyOwner { | |
| // Validate feedId (basic check to ensure it's not empty) | |
| if (bytes(feedId).length == 0) revert InvalidFeedId(feedId); | |
| // Register the asset with its feed ID | |
| assetToFeedId[asset] = feedId; | |
| // Initialize price data for this feed | |
| feedPriceData[feedId] = PriceData({ | |
| price: 0, | |
| decimals: decimals, | |
| timestamp: 0 | |
| }); | |
| // Add feedId to the array if it doesn't already exist | |
| bool exists = false; | |
| for (uint i = 0; i < feedIds.length; i++) { | |
| if (keccak256(bytes(feedIds[i])) == keccak256(bytes(feedId))) { | |
| exists = true; | |
| break; | |
| } | |
| } | |
| if (!exists) { | |
| feedIds.push(feedId); | |
| } | |
| emit AssetFeedAdded(asset, feedId, decimals); | |
| } | |
| /** | |
| * @notice Get the latest price of an asset | |
| * @param asset The address of the asset | |
| * @return price The latest price of the asset | |
| */ | |
| function getAssetPrice(address asset) external view override returns (uint256) { | |
| string memory feedId = assetToFeedId[asset]; | |
| // Check if the asset is registered | |
| if (bytes(feedId).length == 0) revert AssetNotRegistered(asset); | |
| // Return the latest price | |
| return feedPriceData[feedId].price; | |
| } | |
| /** | |
| * @notice Get the feed IDs array | |
| * @return Array of feed IDs | |
| */ | |
| function getFeedIds() external view returns (string[] memory) { | |
| return feedIds; | |
| } | |
| // This function uses revert to convey call information. | |
| // See https://eips.ethereum.org/EIPS/eip-3668#rationale for details. | |
| function checkLog( | |
| Log calldata log, | |
| bytes memory | |
| ) external returns (bool upkeepNeeded, bytes memory performData) { | |
| revert StreamsLookup( | |
| DATASTREAMS_FEEDLABEL, | |
| feedIds, | |
| DATASTREAMS_QUERYLABEL, | |
| log.timestamp, | |
| "" | |
| ); | |
| } | |
| /** | |
| * @notice this is a new, optional function in streams lookup. It is meant to surface streams lookup errors. | |
| * @return upkeepNeeded boolean to indicate whether the keeper should call performUpkeep or not. | |
| * @return performData bytes that the keeper should call performUpkeep with, if | |
| * upkeep is needed. If you would like to encode data to decode later, try `abi.encode`. | |
| */ | |
| function checkErrorHandler( | |
| uint256 /*errCode*/, | |
| bytes memory /*extraData*/ | |
| ) external pure returns (bool upkeepNeeded, bytes memory performData) { | |
| return (true, "0"); | |
| // Hardcoded to always perform upkeep. | |
| // Read the StreamsLookup error handler guide for more information. | |
| // https://docs.chain.link/chainlink-automation/guides/streams-lookup-error-handler | |
| } | |
| // The Data Streams report bytes is passed here. | |
| // extraData is context data from stream lookup process. | |
| // Your contract may include logic to further process this data. | |
| // This method is intended only to be simulated offchain by Automation. | |
| // The data returned will then be passed by Automation into performUpkeep | |
| function checkCallback( | |
| bytes[] calldata values, | |
| bytes calldata extraData | |
| ) external pure returns (bool, bytes memory) { | |
| return (true, abi.encode(values, extraData)); | |
| } | |
| // Function will be performed onchain | |
| function performUpkeep(bytes calldata performData) external { | |
| // Decode the performData bytes passed in by CL Automation. | |
| // This contains the data returned by your implementation in checkCallback(). | |
| (bytes[] memory signedReports, bytes memory extraData) = abi.decode( | |
| performData, | |
| (bytes[], bytes) | |
| ); | |
| IFeeManager feeManager = IFeeManager(address(verifier.s_feeManager())); | |
| IRewardManager rewardManager = IRewardManager( | |
| address(feeManager.i_rewardManager()) | |
| ); | |
| address feeTokenAddress = feeManager.i_linkAddress(); | |
| // Process each signed report | |
| for (uint i = 0; i < signedReports.length; i++) { | |
| bytes memory unverifiedReport = signedReports[i]; | |
| (, /* bytes32[3] reportContextData */ bytes memory reportData) = abi | |
| .decode(unverifiedReport, (bytes32[3], bytes)); | |
| // Extract report version from reportData | |
| uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) | | |
| uint16(uint8(reportData[1])); | |
| // Validate report version | |
| if (reportVersion != 3 && reportVersion != 4) { | |
| revert InvalidReportVersion(uint8(reportVersion)); | |
| } | |
| // Report verification fees | |
| (Common.Asset memory fee, , ) = feeManager.getFeeAndReward( | |
| address(this), | |
| reportData, | |
| feeTokenAddress | |
| ); | |
| // Approve rewardManager to spend this contract's balance in fees | |
| IERC20(feeTokenAddress).approve(address(rewardManager), fee.amount); | |
| // Verify the report | |
| bytes memory verifiedReportData = verifier.verify( | |
| unverifiedReport, | |
| abi.encode(feeTokenAddress) | |
| ); | |
| // Decode verified report data into the appropriate Report struct based on reportVersion | |
| if (reportVersion == 3) { | |
| // v3 report schema | |
| ReportV3 memory verifiedReport = abi.decode( | |
| verifiedReportData, | |
| (ReportV3) | |
| ); | |
| // Convert bytes32 feedId to string for lookup | |
| string memory feedIdStr = bytes32ToString(verifiedReport.feedId); | |
| // Check if we're tracking this feed | |
| if (feedPriceData[feedIdStr].decimals > 0) { | |
| // Convert int192 to uint256 for storage (assuming prices are positive) | |
| uint256 normalizedPrice = uint256(uint192(verifiedReport.price)); | |
| // Update the price data | |
| feedPriceData[feedIdStr].price = normalizedPrice; | |
| feedPriceData[feedIdStr].timestamp = block.timestamp; | |
| emit PriceUpdated(feedIdStr, normalizedPrice, block.timestamp); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * @notice Convert bytes32 to string | |
| * @param _bytes32 The bytes32 value to convert | |
| * @return string representation of the bytes32 value | |
| */ | |
| function bytes32ToString(bytes32 _bytes32) internal pure returns (string memory) { | |
| bytes memory bytesArray = new bytes(64); | |
| for (uint256 i = 0; i < 32; i++) { | |
| bytes1 char = bytes1(bytes32(uint256(_bytes32) * 2 ** (8 * i))); | |
| bytesArray[i*2] = bytes1(uint8(uint256(char) / 16) + (uint8(uint256(char) / 16) < 10 ? 48 : 87)); | |
| bytesArray[i*2+1] = bytes1(uint8(uint256(char) % 16) + (uint8(uint256(char) % 16) < 10 ? 48 : 87)); | |
| } | |
| return string(bytesArray); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity 0.8.19; | |
| // Minimal interface for PriceOracle | |
| interface IPriceOracle { | |
| function getAssetPrice(address asset) external view returns (uint256); | |
| } | |
| // Simplified ERC20 interface | |
| interface IERC20 { | |
| function balanceOf(address account) external view returns (uint256); | |
| function transfer(address recipient, uint256 amount) external returns (bool); | |
| function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); | |
| } | |
| /** | |
| * @title RaaveLendingPool | |
| * @notice Extremely simplified lending pool for demonstration purposes | |
| */ | |
| contract RaaveLendingPool { | |
| // Price oracle contract | |
| IPriceOracle public priceOracle; | |
| // Constant for liquidation threshold (80%) | |
| uint256 public constant LIQUIDATION_THRESHOLD = 8000; | |
| // Assets state | |
| mapping(address => bool) public supportedAssets; | |
| // User state | |
| mapping(address => mapping(address => uint256)) public userCollateral; | |
| mapping(address => mapping(address => uint256)) public userBorrows; | |
| // Events | |
| event Deposit(address indexed user, address indexed asset, uint256 amount); | |
| event Borrow(address indexed user, address indexed asset, uint256 amount); | |
| event Liquidation(address indexed user, address indexed borrowAsset, address indexed collateralAsset, uint256 amount); | |
| constructor(address _priceOracle) { | |
| priceOracle = IPriceOracle(_priceOracle); | |
| } | |
| /** | |
| * @notice Deposit collateral | |
| * @param asset The asset to deposit | |
| * @param amount The amount to deposit | |
| */ | |
| function deposit(address asset, uint256 amount) external { | |
| require(supportedAssets[asset], "Asset not supported"); | |
| // Transfer asset from user | |
| IERC20(asset).transferFrom(msg.sender, address(this), amount); | |
| // Update state | |
| userCollateral[msg.sender][asset] += amount; | |
| emit Deposit(msg.sender, asset, amount); | |
| } | |
| /** | |
| * @notice Borrow asset | |
| * @param asset The asset to borrow | |
| * @param amount The amount to borrow | |
| */ | |
| function borrow(address asset, uint256 amount) external { | |
| require(supportedAssets[asset], "Asset not supported"); | |
| // INTEGRATION POINT 1: Check if user has enough collateral | |
| require(hasEnoughCollateral(msg.sender, asset, amount), "Insufficient collateral"); | |
| // Update state | |
| userBorrows[msg.sender][asset] += amount; | |
| // Transfer asset to user | |
| IERC20(asset).transfer(msg.sender, amount); | |
| emit Borrow(msg.sender, asset, amount); | |
| } | |
| /** | |
| * @notice Liquidate an undercollateralized position | |
| * @param borrower The borrower to liquidate | |
| * @param borrowAsset The borrowed asset | |
| * @param collateralAsset The collateral asset to seize | |
| * @param amount The amount of the borrowed asset to repay | |
| */ | |
| function liquidate(address borrower, address borrowAsset, address collateralAsset, uint256 amount) external { | |
| // INTEGRATION POINT 2: Check if borrower is undercollateralized | |
| require(isUndercollateralized(borrower), "Borrower not undercollateralized"); | |
| uint256 borrowAssetPrice = priceOracle.getAssetPrice(borrowAsset); | |
| uint256 collateralAssetPrice = priceOracle.getAssetPrice(collateralAsset); | |
| // Calculate collateral to seize (with 10% bonus) | |
| uint256 collateralToSeize = (amount * borrowAssetPrice * 110) / (collateralAssetPrice * 100); | |
| // Update state | |
| userBorrows[borrower][borrowAsset] -= amount; | |
| userCollateral[borrower][collateralAsset] -= collateralToSeize; | |
| // Transfer assets | |
| IERC20(borrowAsset).transferFrom(msg.sender, address(this), amount); | |
| IERC20(collateralAsset).transfer(msg.sender, collateralToSeize); | |
| emit Liquidation(borrower, borrowAsset, collateralAsset, amount); | |
| } | |
| /** | |
| * @notice Check if a user has enough collateral to borrow | |
| * @param user The user address | |
| * @param borrowAsset The asset to borrow | |
| * @param borrowAmount The amount to borrow | |
| * @return bool True if user has enough collateral | |
| */ | |
| function hasEnoughCollateral(address user, address borrowAsset, uint256 borrowAmount) public view returns (bool) { | |
| // INTEGRATION POINT 3: Use price oracle to calculate collateral value | |
| address[] memory assets = getSupportedAssets(); | |
| uint256 totalCollateralValueUSD = 0; | |
| uint256 totalBorrowValueUSD = 0; | |
| // Calculate existing borrows in USD | |
| for (uint i = 0; i < assets.length; i++) { | |
| address asset = assets[i]; | |
| uint256 borrowed = userBorrows[user][asset]; | |
| if (borrowed > 0) { | |
| uint256 assetPrice = priceOracle.getAssetPrice(asset); | |
| totalBorrowValueUSD += borrowed * assetPrice; | |
| } | |
| } | |
| // Add new borrow | |
| uint256 borrowAssetPrice = priceOracle.getAssetPrice(borrowAsset); | |
| totalBorrowValueUSD += borrowAmount * borrowAssetPrice; | |
| // Calculate collateral value in USD | |
| for (uint i = 0; i < assets.length; i++) { | |
| address asset = assets[i]; | |
| uint256 collateral = userCollateral[user][asset]; | |
| if (collateral > 0) { | |
| uint256 assetPrice = priceOracle.getAssetPrice(asset); | |
| totalCollateralValueUSD += collateral * assetPrice; | |
| } | |
| } | |
| // Apply liquidation threshold | |
| uint256 maxBorrowValue = (totalCollateralValueUSD * LIQUIDATION_THRESHOLD) / 10000; | |
| return totalBorrowValueUSD <= maxBorrowValue; | |
| } | |
| /** | |
| * @notice Check if a user is undercollateralized | |
| * @param user The user address | |
| * @return bool True if user is undercollateralized | |
| */ | |
| function isUndercollateralized(address user) public view returns (bool) { | |
| // INTEGRATION POINT 4: Use price oracle to calculate health factor | |
| address[] memory assets = getSupportedAssets(); | |
| uint256 totalCollateralValueUSD = 0; | |
| uint256 totalBorrowValueUSD = 0; | |
| // Calculate borrows in USD | |
| for (uint i = 0; i < assets.length; i++) { | |
| address asset = assets[i]; | |
| uint256 borrowed = userBorrows[user][asset]; | |
| if (borrowed > 0) { | |
| uint256 assetPrice = priceOracle.getAssetPrice(asset); | |
| totalBorrowValueUSD += borrowed * assetPrice; | |
| } | |
| } | |
| // No borrows, not undercollateralized | |
| if (totalBorrowValueUSD == 0) { | |
| return false; | |
| } | |
| // Calculate collateral value in USD | |
| for (uint i = 0; i < assets.length; i++) { | |
| address asset = assets[i]; | |
| uint256 collateral = userCollateral[user][asset]; | |
| if (collateral > 0) { | |
| uint256 assetPrice = priceOracle.getAssetPrice(asset); | |
| totalCollateralValueUSD += collateral * assetPrice; | |
| } | |
| } | |
| // Apply liquidation threshold | |
| uint256 maxBorrowValue = (totalCollateralValueUSD * LIQUIDATION_THRESHOLD) / 10000; | |
| return totalBorrowValueUSD > maxBorrowValue; | |
| } | |
| /** | |
| * @notice Get array of supported assets | |
| * @return Array of asset addresses | |
| */ | |
| function getSupportedAssets() internal view returns (address[] memory) { | |
| address[] memory assets = new address[](1); | |
| assets[0] = address(0x1); // ETH placeholder | |
| return assets; | |
| } | |
| /** | |
| * @notice Add supported asset (admin function) | |
| * @param asset The asset to add | |
| */ | |
| function addSupportedAsset(address asset) external { | |
| // In a real implementation, this would have access control | |
| supportedAssets[asset] = true; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment