-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
- Loading branch information
Showing
19 changed files
with
1,792 additions
and
0 deletions.
There are no files selected for viewing
46 changes: 46 additions & 0 deletions
46
src/adapters/destination-adapters/ChronicleMedianDestinationAdapter.sol
This file contains 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
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
46
src/adapters/destination-adapters/OSMDestinationAdapter.sol
This file contains 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
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
86
src/adapters/destination-adapters/PythDestinationAdapter.sol
This file contains 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
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
118
src/adapters/source-adapters/BoundedUnionSourceAdapter.sol
This file contains 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
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
53
src/adapters/source-adapters/ChronicleMedianSourceAdapter.sol
This file contains 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
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); | ||
} | ||
} |
Oops, something went wrong.