Skip to content

Instantly share code, notes, and snippets.

@mayorcoded
Created March 24, 2025 09:35
Show Gist options
  • Select an option

  • Save mayorcoded/938d5f681c29baf2feed5c2a5e7463a0 to your computer and use it in GitHub Desktop.

Select an option

Save mayorcoded/938d5f681c29baf2feed5c2a5e7463a0 to your computer and use it in GitHub Desktop.
RAAVE Data Stream Integrations
// 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);
}
}
// 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