Skip to content

Commit

Permalink
Merge branch 'master' into pablo/redstone-adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
md0x committed May 20, 2024
2 parents 282fbba + e52f84a commit 48ebe72
Show file tree
Hide file tree
Showing 54 changed files with 1,694 additions and 186 deletions.
29 changes: 27 additions & 2 deletions src/DiamondRootOval.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,48 @@ abstract contract DiamondRootOval is IBaseController, IOval, IBaseOracleAdapter
*/
function getLatestSourceData() public view virtual returns (int256, uint256);

/**
* @notice Returns the requested round data from the source.
* @dev If the source does not support rounds this would return uninitialized data.
* @param roundId The roundId to retrieve the round data for.
* @return answer Round answer in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function getSourceDataAtRound(uint256 roundId) public view virtual returns (int256, uint256);

/**
* @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.
* @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.
* @return roundId The roundId of the answer.
*/
function tryLatestDataAt(uint256 timestamp, uint256 maxTraversal) public view virtual returns (int256, uint256);
function tryLatestDataAt(uint256 timestamp, uint256 maxTraversal)
public
view
virtual
returns (int256, uint256, uint256);

/**
* @notice Returns the latest data from the source. Depending on when Oval was last unlocked this might
* return an slightly stale value to protect the OEV from being stolen by a front runner.
* @return answer The latest answer in 18 decimals.
* @return updatedAt The timestamp of the answer.
* @return roundId The roundId of the answer.
*/
function internalLatestData() public view virtual returns (int256, uint256, uint256);

/**
* @notice Returns the requested round data from the source. Depending on when Oval was last unlocked this might
* also return uninitialized value to protect the OEV from being stolen by a front runner.
* @dev If the source does not support rounds this would always return uninitialized data.
* @param roundId The roundId to retrieve the round data for.
* @return answer Round answer in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function internalLatestData() public view virtual returns (int256, uint256);
function internalDataAtRound(uint256 roundId) public view virtual returns (int256, uint256);

/**
* @notice Snapshot the current source data. Is a no-op if the source does not require snapshotting.
Expand Down
22 changes: 21 additions & 1 deletion src/Oval.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,31 @@ abstract contract Oval is DiamondRootOval {
* @notice Returns latest data from source, governed by lockWindow controlling if returned data is stale.
* @return answer The latest answer in 18 decimals.
* @return timestamp The timestamp of the answer.
* @return roundId The roundId of the answer.
*/
function internalLatestData() public view override returns (int256, uint256) {
function internalLatestData() public view override returns (int256, uint256, uint256) {
// Case work:
//-> If unlockLatestValue has been called within lockWindow, then return most recent price as of unlockLatestValue call.
//-> If unlockLatestValue has not been called in lockWindow, then return most recent value that is at least lockWindow old.
return tryLatestDataAt(Math.max(lastUnlockTime, block.timestamp - lockWindow()), maxTraversal());
}

/**
* @notice Returns the requested round data from the source. Depending on when Oval was last unlocked this might
* also return uninitialized values to protect the OEV from being stolen by a front runner.
* @dev If the source does not support rounds this would always return uninitialized data.
* @param roundId The roundId to retrieve the round data for.
* @return answer Round answer in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {
(int256 answer, uint256 timestamp) = getSourceDataAtRound(roundId);

// Return source data for the requested round only if it has been either explicitly or implicitly unlocked:
//-> explicit unlock when source time is not newer than the time when last unlockLatestValue has been called, or
//-> implicit unlock when source data is at least lockWindow old.
uint256 latestUnlockedTimestamp = Math.max(lastUnlockTime, block.timestamp - lockWindow());
if (timestamp <= latestUnlockedTimestamp) return (answer, timestamp);
return (0, 0); // Source data is too recent, return uninitialized values.
}
}
4 changes: 2 additions & 2 deletions src/adapters/destination-adapters/BaseDestinationAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ abstract contract BaseDestinationAdapter is DiamondRootOval {
* @return answer The latest answer in 18 decimals.
*/
function latestAnswer() public view returns (int256) {
(int256 answer,) = internalLatestData();
(int256 answer,,) = internalLatestData();
return answer;
}

Expand All @@ -25,7 +25,7 @@ abstract contract BaseDestinationAdapter is DiamondRootOval {
* @return timestamp The timestamp of the most recent update.
*/
function latestTimestamp() public view returns (uint256) {
(, uint256 timestamp) = internalLatestData();
(, uint256 timestamp,) = internalLatestData();
return timestamp;
}
}
34 changes: 26 additions & 8 deletions src/adapters/destination-adapters/ChainlinkDestinationAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

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

import {DecimalLib} from "../lib/DecimalLib.sol";
import {IAggregatorV3} from "../../interfaces/chainlink/IAggregatorV3.sol";
import {DiamondRootOval} from "../../DiamondRootOval.sol";
Expand All @@ -24,7 +26,7 @@ abstract contract ChainlinkDestinationAdapter is DiamondRootOval, IAggregatorV3
* @return answer The latest answer in the configured number of decimals.
*/
function latestAnswer() public view override returns (int256) {
(int256 answer,) = internalLatestData();
(int256 answer,,) = internalLatestData();
return DecimalLib.convertDecimals(answer, 18, decimals);
}

Expand All @@ -33,21 +35,37 @@ abstract contract ChainlinkDestinationAdapter is DiamondRootOval, IAggregatorV3
* @return timestamp The timestamp of the latest answer.
*/
function latestTimestamp() public view override returns (uint256) {
(, uint256 timestamp) = internalLatestData();
(, uint256 timestamp,) = internalLatestData();
return timestamp;
}

/**
* @notice Returns an approximate form of the latest Round data. This does not implement the notion of "roundId" that
* the normal chainlink aggregator does and returns hardcoded values for those fields.
* @return roundId The roundId of the latest answer, hardcoded to 1.
* @notice Returns the latest Round data.
* @return roundId The roundId of the latest answer (sources that do not support it hardcodes to 1).
* @return answer The latest answer in the configured number of decimals.
* @return startedAt The timestamp when the value was updated.
* @return updatedAt The timestamp when the value was updated.
* @return answeredInRound The roundId of the round in which the answer was computed, hardcoded to 1.
* @return answeredInRound The roundId of the round in which the answer was computed (sources that do not support it
* hardcodes to 1).
*/
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
(int256 answer, uint256 updatedAt) = internalLatestData();
return (1, DecimalLib.convertDecimals(answer, 18, decimals), updatedAt, updatedAt, 1);
(int256 answer, uint256 updatedAt, uint256 _roundId) = internalLatestData();
uint80 roundId = SafeCast.toUint80(_roundId);
return (roundId, DecimalLib.convertDecimals(answer, 18, decimals), updatedAt, updatedAt, roundId);
}

/**
* @notice Returns the requested round data if available or uninitialized values then it is too recent.
* @dev If the source does not support round data, always returns uninitialized answer and timestamp values.
* @param _roundId The roundId to retrieve the round data for.
* @return roundId The roundId of the latest answer (same as requested roundId).
* @return answer The latest answer in the configured number of decimals.
* @return startedAt The timestamp when the value was updated.
* @return updatedAt The timestamp when the value was updated.
* @return answeredInRound The roundId of the round in which the answer was computed (same as requested roundId).
*/
function getRoundData(uint80 _roundId) external view returns (uint80, int256, uint256, uint256, uint80) {
(int256 answer, uint256 updatedAt) = internalDataAtRound(_roundId);
return (_roundId, DecimalLib.convertDecimals(answer, 18, decimals), updatedAt, updatedAt, _roundId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ abstract contract ChronicleMedianDestinationAdapter is IMedian, DiamondRootOval
* @return answer The latest answer in 18 decimals.
*/
function read() public view override returns (uint256) {
(int256 answer,) = internalLatestData();
(int256 answer,,) = internalLatestData();
require(answer > 0, "Median/invalid-price-feed");
return uint256(answer);
}
Expand All @@ -30,7 +30,7 @@ abstract contract ChronicleMedianDestinationAdapter is IMedian, DiamondRootOval
* @return valid True if the value returned is valid.
*/
function peek() public view override returns (uint256, bool) {
(int256 answer,) = internalLatestData();
(int256 answer,,) = internalLatestData();
return (uint256(answer), answer > 0);
}

Expand All @@ -39,7 +39,7 @@ abstract contract ChronicleMedianDestinationAdapter is IMedian, DiamondRootOval
* @return timestamp The timestamp of the most recent update.
*/
function age() public view override returns (uint32) {
(, uint256 timestamp) = internalLatestData();
(, uint256 timestamp,) = internalLatestData();
return uint32(timestamp);
}
}
6 changes: 3 additions & 3 deletions src/adapters/destination-adapters/OSMDestinationAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ abstract contract OSMDestinationAdapter is IOSM, DiamondRootOval {
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();
(int256 answer,,) = internalLatestData();
return bytes32(uint256(answer));
}

Expand All @@ -29,7 +29,7 @@ abstract contract OSMDestinationAdapter is IOSM, DiamondRootOval {
* @return valid True if the value returned is valid.
*/
function peek() public view override returns (bytes32, bool) {
(int256 answer,) = internalLatestData();
(int256 answer,,) = internalLatestData();
// This might be required for MakerDAO when voiding Oracle sources.
return (bytes32(uint256(answer)), answer > 0);
}
Expand All @@ -39,7 +39,7 @@ abstract contract OSMDestinationAdapter is IOSM, DiamondRootOval {
* @return timestamp The timestamp of the most recent update.
*/
function zzz() public view override returns (uint64) {
(, uint256 timestamp) = internalLatestData();
(, uint256 timestamp,) = internalLatestData();
return uint64(timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ contract PythDestinationAdapter is Ownable, IPyth {
if (address(idToOval[id]) == address(0)) {
return basePythProvider.getPriceUnsafe(id);
}
(int256 answer, uint256 timestamp) = idToOval[id].internalLatestData();
(int256 answer, uint256 timestamp,) = idToOval[id].internalLatestData();
return Price({
price: SafeCast.toInt64(DecimalLib.convertDecimals(answer, 18, idToDecimal[id])),
conf: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract UniswapAnchoredViewDestinationAdapter is Ownable, IUniswapAnchoredView
if (cTokenToOval[cToken] == address(0)) {
return uniswapAnchoredViewSource.getUnderlyingPrice(cToken);
}
(int256 answer,) = IOval(cTokenToOval[cToken]).internalLatestData();
(int256 answer,,) = IOval(cTokenToOval[cToken]).internalLatestData();
return DecimalLib.convertDecimals(uint256(answer), 18, cTokenToDecimal[cToken]);
}

Expand Down
25 changes: 22 additions & 3 deletions src/adapters/source-adapters/BoundedUnionSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ abstract contract BoundedUnionSourceAdapter is
return _selectBoundedPrice(clAnswer, clTimestamp, crAnswer, crTimestamp, pyAnswer, pyTimestamp);
}

/**
* @notice Returns the requested round data from the source.
* @dev Not all aggregated adapters support this, so this returns uninitialized data.
* @return answer Round answer in 18 decimals.
* @return updatedAt The timestamp of the answer.
*/
function getSourceDataAtRound(uint256 /* roundId */ )
public
view
virtual
override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter)
returns (int256, uint256)
{
return (0, 0);
}

/**
* @notice Snapshots is a no-op for this adapter as its never used.
*/
Expand All @@ -67,15 +83,16 @@ abstract contract BoundedUnionSourceAdapter is
* @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.
* @return roundId The roundId of the answer (hardcoded to 1 as not all aggregated adapters support it).
*/
function tryLatestDataAt(uint256 timestamp, uint256 maxTraversal)
public
view
override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter)
returns (int256, uint256)
returns (int256, uint256, 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);
(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();
Expand All @@ -86,7 +103,9 @@ abstract contract BoundedUnionSourceAdapter is
if (crTimestamp > timestamp) crTimestamp = 0;
if (pyTimestamp > timestamp) pyTimestamp = 0;

return _selectBoundedPrice(clAnswer, clTimestamp, crAnswer, crTimestamp, pyAnswer, pyTimestamp);
(int256 boundedAnswer, uint256 boundedTimestamp) =
_selectBoundedPrice(clAnswer, clTimestamp, crAnswer, crTimestamp, pyAnswer, pyTimestamp);
return (boundedAnswer, boundedTimestamp, 1);
}

// Selects the appropriate price from the three sources based on the bounding tolerance and logic.
Expand Down

0 comments on commit 48ebe72

Please sign in to comment.