Skip to content

Commit

Permalink
feat: add audited contracts
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
  • Loading branch information
mrice32 committed May 16, 2024
1 parent dfd27ac commit 97d60b0
Show file tree
Hide file tree
Showing 19 changed files with 1,792 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import {IMedian} from "../../interfaces/chronicle/IMedian.sol";
import {DiamondRootOval} from "../../DiamondRootOval.sol";

/**
* @notice ChronicleMedianDestinationAdapter contract to expose Oval data via the standard Chronicle interface.
*/

abstract contract ChronicleMedianDestinationAdapter is IMedian, DiamondRootOval {
constructor(address _sourceAdapter) {}

uint8 public constant decimals = 18; // Chronicle price feeds have always have 18 decimals.

/**
* @notice Returns the latest data from the source.
* @dev The standard chronicle implementation will revert if the latest answer is not valid when calling the read
* function. This implementation will only revert if the latest answer is negative.
* @return answer The latest answer in 18 decimals.
*/
function read() public view override returns (uint256) {
(int256 answer,) = internalLatestData();
require(answer > 0, "Median/invalid-price-feed");
return uint256(answer);
}

/**
* @notice Returns the latest data from the source and a bool indicating if the value is valid.
* @return answer The latest answer in 18 decimals.
* @return valid True if the value returned is valid.
*/
function peek() public view override returns (uint256, bool) {
(int256 answer,) = internalLatestData();
return (uint256(answer), answer > 0);
}

/**
* @notice Returns the timestamp of the most recently updated data.
* @return timestamp The timestamp of the most recent update.
*/
function age() public view override returns (uint32) {
(, uint256 timestamp) = internalLatestData();
return uint32(timestamp);
}
}
46 changes: 46 additions & 0 deletions src/adapters/destination-adapters/OSMDestinationAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import {IOSM} from "../../interfaces/makerdao/IOSM.sol";
import {DiamondRootOval} from "../../DiamondRootOval.sol";

/**
* @title OSMDestinationAdapter contract to expose Oval data via the standard MakerDAO OSM interface.
*/

abstract contract OSMDestinationAdapter is IOSM, DiamondRootOval {
constructor() {}

/**
* @notice Returns the latest data from the source, formatted for the OSM interface as a bytes32.
* @dev The standard OSM implementation will revert if the latest answer is not valid when calling the read function.
* This implementation will only revert if the latest answer is negative.
* @return answer The latest answer as a bytes32.
*/
function read() public view override returns (bytes32) {
// MakerDAO performs decimal conversion in collateral adapter contracts, so all oracle prices are expected to
// have 18 decimals, the same as returned by the internalLatestData().answer.
(int256 answer,) = internalLatestData();
return bytes32(uint256(answer));
}

/**
* @notice Returns the latest data from the source and a bool indicating if the value is valid.
* @return answer The latest answer as a bytes32.
* @return valid True if the value returned is valid.
*/
function peek() public view override returns (bytes32, bool) {
(int256 answer,) = internalLatestData();
// This might be required for MakerDAO when voiding Oracle sources.
return (bytes32(uint256(answer)), answer > 0);
}

/**
* @notice Returns the timestamp of the most recently updated data.
* @return timestamp The timestamp of the most recent update.
*/
function zzz() public view override returns (uint64) {
(, uint256 timestamp) = internalLatestData();
return uint64(timestamp);
}
}
86 changes: 86 additions & 0 deletions src/adapters/destination-adapters/PythDestinationAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol";

import {IPyth} from "../../interfaces/pyth/IPyth.sol";
import {IOval} from "../../interfaces/IOval.sol";
import {DecimalLib} from "../lib/DecimalLib.sol";

/**
* @notice PythDestinationAdapter contract to expose Oval data via the standard pyth interface.
*/
contract PythDestinationAdapter is Ownable, IPyth {
mapping(bytes32 => IOval) public idToOval;
mapping(bytes32 => uint8) public idToDecimal;
mapping(bytes32 => uint256) public idToValidTimePeriod;

IPyth public immutable basePythProvider;

event BaseSourceSet(address indexed sourceOracle);
event OvalSet(bytes32 indexed id, uint8 indexed decimals, uint256 validTimePeriod, address indexed oval);

constructor(IPyth _basePythProvider) {
basePythProvider = _basePythProvider;

emit BaseSourceSet(address(_basePythProvider));
}

/**
* @notice Enables the owner to set mapping between pyth identifiers and Ovals. Done for each identifier.
* @param id The pyth identifier to set the Oval for.
* @param decimals The number of decimals for the identifier.
* @param validTimePeriod The number of seconds that a price is valid for.
* @param oval The Oval to set for the identifier.
*/
function setOval(bytes32 id, uint8 decimals, uint256 validTimePeriod, IOval oval) public onlyOwner {
idToOval[id] = oval;
idToDecimal[id] = decimals;
idToValidTimePeriod[id] = validTimePeriod;

emit OvalSet(id, decimals, validTimePeriod, address(oval));
}

/**
* @notice Returns the price for the given identifier. This function does not care if the price is too old.
* @param id The pyth identifier to get the price for.
* @return price the standard pyth price struct.
*/
function getPriceUnsafe(bytes32 id) public view returns (Price memory) {
if (address(idToOval[id]) == address(0)) {
return basePythProvider.getPriceUnsafe(id);
}
(int256 answer, uint256 timestamp) = idToOval[id].internalLatestData();
return Price({
price: SafeCast.toInt64(DecimalLib.convertDecimals(answer, 18, idToDecimal[id])),
conf: 0,
expo: -int32(uint32(idToDecimal[id])),
publishTime: timestamp
});
}

/**
* @notice Function to get price.
* @dev in pyth, this function reverts if the returned price isn't older than a configurable number of seconds.
* idToValidTimePeriod[id] is that number of seconds in this contract.
* @param id The pyth identifier to get the price for.
* @return price the standard pyth price struct.
*/
function getPrice(bytes32 id) external view returns (Price memory) {
if (address(idToOval[id]) == address(0)) {
return basePythProvider.getPrice(id);
}
Price memory price = getPriceUnsafe(id);
require(_diff(block.timestamp, price.publishTime) <= idToValidTimePeriod[id], "Not within valid window");
return price;
}

// Internal function to get absolute difference between two numbers. This implementation replicates diff function
// logic from AbstractPyth contract used in Pyth oracle:
// https://github.com/pyth-network/pyth-sdk-solidity/blob/c24b3e0173a5715c875ae035c20e063cb900f481/AbstractPyth.sol#L79
function _diff(uint256 x, uint256 y) internal pure returns (uint256) {
if (x > y) return x - y;
return y - x;
}
}
118 changes: 118 additions & 0 deletions src/adapters/source-adapters/BoundedUnionSourceAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import {SignedMath} from "openzeppelin-contracts/contracts/utils/math/SignedMath.sol";

import {IAggregatorV3Source} from "../../interfaces/chainlink/IAggregatorV3Source.sol";
import {IMedian} from "../../interfaces/chronicle/IMedian.sol";
import {IPyth} from "../../interfaces/pyth/IPyth.sol";
import {ChainlinkSourceAdapter} from "./ChainlinkSourceAdapter.sol";
import {ChronicleMedianSourceAdapter} from "./ChronicleMedianSourceAdapter.sol";
import {PythSourceAdapter} from "./PythSourceAdapter.sol";
import {SnapshotSource} from "./SnapshotSource.sol";

/**
* @title BoundedUnionSourceAdapter contract to read data from multiple sources and return the newest, contingent on it
* being within a certain tolerance of the other sources. The return logic operates as follows:
* a) Return the most recent price if it's within tolerance of at least one of the other two.
* b) If not, return the second most recent price if it's within tolerance of at least one of the other two.
* c) If neither a) nor b) is met, return the chainlink price.
* @dev This adapter only works with Chainlink, Chronicle and Pyth adapters. If alternative adapter configs are desired
* then a new adapter should be created.
*/

abstract contract BoundedUnionSourceAdapter is
ChainlinkSourceAdapter,
ChronicleMedianSourceAdapter,
PythSourceAdapter
{
uint256 public immutable BOUNDING_TOLERANCE;

constructor(
IAggregatorV3Source chainlink,
IMedian chronicle,
IPyth pyth,
bytes32 pythPriceId,
uint256 boundingTolerance
) ChainlinkSourceAdapter(chainlink) ChronicleMedianSourceAdapter(chronicle) PythSourceAdapter(pyth, pythPriceId) {
BOUNDING_TOLERANCE = boundingTolerance;
}

/**
* @notice Returns the latest data from the source, contingent on it being within a tolerance of the other sources.
* @return answer The latest answer in 18 decimals.
* @return timestamp The timestamp of the answer.
*/
function getLatestSourceData()
public
view
override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter)
returns (int256 answer, uint256 timestamp)
{
(int256 clAnswer, uint256 clTimestamp) = ChainlinkSourceAdapter.getLatestSourceData();
(int256 crAnswer, uint256 crTimestamp) = ChronicleMedianSourceAdapter.getLatestSourceData();
(int256 pyAnswer, uint256 pyTimestamp) = PythSourceAdapter.getLatestSourceData();

return _selectBoundedPrice(clAnswer, clTimestamp, crAnswer, crTimestamp, pyAnswer, pyTimestamp);
}

/**
* @notice Snapshots is a no-op for this adapter as its never used.
*/
function snapshotData() public override(ChainlinkSourceAdapter, SnapshotSource) {}

/**
* @notice Tries getting latest data as of requested timestamp. Note that for all historic lookups we simply return
* the Chainlink data as this is the only supported source that has historical data.
* @param timestamp The timestamp to try getting latest data at.
* @param maxTraversal The maximum number of rounds to traverse when looking for historical data.
* @return answer The answer as of requested timestamp, or earliest available data if not available, in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function tryLatestDataAt(uint256 timestamp, uint256 maxTraversal)
public
view
override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter)
returns (int256, uint256)
{
// Chainlink has price history, so use tryLatestDataAt to pull the most recent price that satisfies the timestamp constraint.
(int256 clAnswer, uint256 clTimestamp) = ChainlinkSourceAdapter.tryLatestDataAt(timestamp, maxTraversal);

// For Chronicle and Pyth, just pull the most recent prices and drop them if they don't satisfy the constraint.
(int256 crAnswer, uint256 crTimestamp) = ChronicleMedianSourceAdapter.getLatestSourceData();
(int256 pyAnswer, uint256 pyTimestamp) = PythSourceAdapter.getLatestSourceData();

// To "drop" Chronicle and Pyth, we set their timestamps to 0 (as old as possible) if they are too recent.
// This means that they will never be used if either or both are 0.
if (crTimestamp > timestamp) crTimestamp = 0;
if (pyTimestamp > timestamp) pyTimestamp = 0;

return _selectBoundedPrice(clAnswer, clTimestamp, crAnswer, crTimestamp, pyAnswer, pyTimestamp);
}

// Selects the appropriate price from the three sources based on the bounding tolerance and logic.
function _selectBoundedPrice(int256 cl, uint256 clT, int256 cr, uint256 crT, int256 py, uint256 pyT)
internal
view
returns (int256, uint256)
{
int256 newestVal = 0;
uint256 newestT = 0;

// For each price, check if it is within tolerance of the other two. If so, check if it is the newest.
if (pyT > newestT && (_withinTolerance(py, cr) || _withinTolerance(py, cl))) (newestVal, newestT) = (py, pyT);
if (crT > newestT && (_withinTolerance(cr, py) || _withinTolerance(cr, cl))) (newestVal, newestT) = (cr, crT);
if (clT > newestT && (_withinTolerance(cl, py) || _withinTolerance(cl, cr))) (newestVal, newestT) = (cl, clT);

if (newestT == 0) return (cl, clT); // If no valid price was found, default to returning chainlink.

return (newestVal, newestT);
}

// Checks if value a is within tolerance of value b.
function _withinTolerance(int256 a, int256 b) internal view returns (bool) {
uint256 diff = SignedMath.abs(a - b);
uint256 maxDiff = SignedMath.abs(b) * BOUNDING_TOLERANCE / 1e18;
return diff <= maxDiff;
}
}
53 changes: 53 additions & 0 deletions src/adapters/source-adapters/ChronicleMedianSourceAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.17;

import {SnapshotSource} from "./SnapshotSource.sol";
import {IMedian} from "../../interfaces/chronicle/IMedian.sol";
import {SafeCast} from "openzeppelin-contracts/contracts/utils/math/SafeCast.sol";

/**
* @title ChronicleMedianSourceAdapter contract to read data from Chronicle and standardize it for Oval.
*/

abstract contract ChronicleMedianSourceAdapter is SnapshotSource {
IMedian public immutable CHRONICLE_SOURCE;

event SourceSet(address indexed sourceOracle);

constructor(IMedian _chronicleSource) {
CHRONICLE_SOURCE = _chronicleSource;

emit SourceSet(address(_chronicleSource));
}

/**
* @notice Returns the latest data from the source.
* @dev The standard chronicle implementation will revert if the latest answer is not valid when calling the read
* function. Additionally, chronicle returns the answer in 18 decimals, so no conversion is needed.
* @return answer The latest answer in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function getLatestSourceData() public view virtual override returns (int256, uint256) {
return (SafeCast.toInt256(CHRONICLE_SOURCE.read()), CHRONICLE_SOURCE.age());
}

/**
* @notice Tries getting latest data as of requested timestamp. If this is not possible, returns the earliest data
* available past the requested timestamp within provided traversal limitations.
* @dev Chronicle does not support historical lookups so this uses SnapshotSource to get historic data.
* @param timestamp The timestamp to try getting latest data at.
* @param maxTraversal The maximum number of rounds to traverse when looking for historical data.
* @return answer The answer as of requested timestamp, or earliest available data if not available, in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function tryLatestDataAt(uint256 timestamp, uint256 maxTraversal)
public
view
virtual
override
returns (int256, uint256)
{
Snapshot memory snapshot = _tryLatestDataAt(timestamp, maxTraversal);
return (snapshot.answer, snapshot.timestamp);
}
}
Loading

0 comments on commit 97d60b0

Please sign in to comment.