From ce26902df09e3c891421d486bdecfa3cbf8fb318 Mon Sep 17 00:00:00 2001 From: rbajollari Date: Mon, 13 May 2024 09:07:22 -0600 Subject: [PATCH] implement latestRound and getRoundData --- contracts/pricefeed/CloneFactory.sol | 14 +++--- contracts/pricefeed/PriceFeed.sol | 71 ++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/contracts/pricefeed/CloneFactory.sol b/contracts/pricefeed/CloneFactory.sol index 9607222..7cf565c 100644 --- a/contracts/pricefeed/CloneFactory.sol +++ b/contracts/pricefeed/CloneFactory.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.20; import "./PriceFeed.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; -/// @title Factory for creating PriceFeed contract clones -/// @notice This contract will create a PriceFeed clone and map its address to clone creator -/// @dev Cloning is done with OpenZeppelin's Clones contract and clones are initialized by an intitializer function instead of a constructor upon creation +/// @title Factory for creating PriceFeed contract clones. +/// @notice This contract will create a PriceFeed clone and map its address to the clone creator. +/// @dev Cloning is done with OpenZeppelin's Clones contract. contract CloneFactory { event PriceFeedCloneCreated( address _priceFeedCloneAddress @@ -15,15 +15,15 @@ contract CloneFactory { mapping (address => address) public PriceFeedCloneAddresses; address public implementationAddress; - /// @param _implementationAddress Address of implementation contract to be cloned + /// @param _implementationAddress Address of implementation contract to be cloned. constructor(address _implementationAddress) { implementationAddress = _implementationAddress; } - /// @notice Create clone of PriceFeed contract and initialize it - /// @dev Clone method returns address of created clone + /// @notice Create clone of PriceFeed contract and initialize it. + /// @dev Clone method returns address of created clone. /// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. - /// @param _priceFeedDescription Description of PriceFeed. + /// @param _priceFeedDescription Description of PriceFeed, should be set to asset symbol ticker. function createPriceFeed(uint8 _priceFeedDecimals, string calldata _priceFeedDescription) external { address priceFeedCloneAddress = Clones.clone(implementationAddress); PriceFeed(priceFeedCloneAddress).initialize(_priceFeedDecimals, _priceFeedDescription); diff --git a/contracts/pricefeed/PriceFeed.sol b/contracts/pricefeed/PriceFeed.sol index a7c0d72..48c760f 100644 --- a/contracts/pricefeed/PriceFeed.sol +++ b/contracts/pricefeed/PriceFeed.sol @@ -4,7 +4,10 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "../IOjo.sol"; +import "../OjoTypes.sol"; +/// @title Contract for calling Ojo's oracle contract with chainlink's AggregatorV3Interface implemented. +/// @author Ojo Network (https://docs.ojo.network/) contract PriceFeed is Initializable, AggregatorV3Interface { uint8 public priceFeedDecimals; @@ -12,12 +15,22 @@ contract PriceFeed is Initializable, AggregatorV3Interface { IOjo public immutable ojo; + uint80 constant DEFAULT_ROUND = 1; + + uint256 constant DEFAULT_VERSION = 1; + + uint256 internal constant INT256_MAX = uint256(type(int256).max); + + error GetRoundDataCanBeOnlyCalledWithLatestRound(uint80 requestedRoundId); + + error UnsafeUintToIntConversion(uint256 value); + constructor(address ojo_) { ojo = IOjo(ojo_); } - /// @notice Initialize clone of this contract - /// @dev This function is used in place of a constructor in proxy contracts + /// @notice Initialize clone of this contract. + /// @dev This function is used in place of a constructor in proxy contracts. /// @param _priceFeedDecimals Amount of decimals a PriceFeed is denominiated in. /// @param _priceFeedDescription Description of PriceFeed. function initialize(uint8 _priceFeedDecimals, string calldata _priceFeedDescription) @@ -27,18 +40,35 @@ contract PriceFeed is Initializable, AggregatorV3Interface { priceFeedDescription = _priceFeedDescription; } + /// @notice Amount of decimals price is denominated in. function decimals() external view returns (uint8) { return priceFeedDecimals; } + /// @notice Asset that this proxy is tracking. + /// @dev This should be set as the asset symbol ticker as it used to query the Ojo contract. function description() external view returns (string memory) { return priceFeedDescription; } + /// @notice Version always returns 1. function version() external view returns (uint256) { - return 1; + return DEFAULT_VERSION; + } + + /// @dev Latest round always returns 1 since this contract does not support rounds. + function latestRound() public pure returns (uint80) { + return DEFAULT_ROUND; } + /// @notice Fetches price data from Ojo contract from a specified round. + /// @dev Even though rounds are not utilized in this contract getRoundData is implemented for contracts + /// that still rely on it. Function will revert if specified round is not the latest round. + /// @return roundId Round ID of price data, this is always set to 1. + /// @return answer Price in USD of asset this contract is tracking. + /// @return startedAt Timestamp relating to price update. + /// @return updatedAt Timestamp relating to price update. + /// @return answeredInRound Equal to round ID. function getRoundData(uint80 _roundId) external view @@ -49,11 +79,20 @@ contract PriceFeed is Initializable, AggregatorV3Interface { uint256 updatedAt, uint80 answeredInRound ) { - + if (_roundId != latestRound()) { + revert GetRoundDataCanBeOnlyCalledWithLatestRound(_roundId); + } + return latestRoundData(); } + /// @notice Fetches latest price data from Ojo contract. + /// @return roundId Round ID of price data, this is always set to 1. + /// @return answer Price in USD of asset this contract is tracking. + /// @return startedAt Timestamp relating to price update. + /// @return updatedAt Timestamp relating to price update. + /// @return answeredInRound Equal to round ID. function latestRoundData() - external + public view returns ( uint80 roundId, @@ -62,6 +101,28 @@ contract PriceFeed is Initializable, AggregatorV3Interface { uint256 updatedAt, uint80 answeredInRound ) { + roundId = latestRound(); + bytes32 assetName = bytes32(bytes(priceFeedDescription)); + + OjoTypes.PriceData memory priceData = ojo.getPriceData(assetName); + + if (priceData.price > INT256_MAX) { + revert UnsafeUintToIntConversion(priceData.price); + } + + // These values are equal after chainlink’s OCR update + startedAt = priceData.resolveTime; + updatedAt = priceData.resolveTime; + + // roundId is always equal to answeredInRound + answeredInRound = roundId; + return ( + roundId, + int256(priceData.price), + startedAt, + updatedAt, + answeredInRound + ); } }