diff --git a/src/DiamondRootOval.sol b/src/DiamondRootOval.sol index 7fe4c9d..c8223eb 100644 --- a/src/DiamondRootOval.sol +++ b/src/DiamondRootOval.sol @@ -18,6 +18,15 @@ 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. @@ -42,6 +51,16 @@ abstract contract DiamondRootOval is IBaseController, IOval, IBaseOracleAdapter */ 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 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. */ diff --git a/src/Oval.sol b/src/Oval.sol index 3c46bbc..d519d09 100644 --- a/src/Oval.sol +++ b/src/Oval.sol @@ -46,4 +46,23 @@ abstract contract Oval is DiamondRootOval { //-> 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. + } } diff --git a/src/adapters/destination-adapters/ChainlinkDestinationAdapter.sol b/src/adapters/destination-adapters/ChainlinkDestinationAdapter.sol index a147e36..358ae42 100644 --- a/src/adapters/destination-adapters/ChainlinkDestinationAdapter.sol +++ b/src/adapters/destination-adapters/ChainlinkDestinationAdapter.sol @@ -53,4 +53,19 @@ abstract contract ChainlinkDestinationAdapter is DiamondRootOval, IAggregatorV3 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); + } } diff --git a/src/adapters/source-adapters/ChainlinkSourceAdapter.sol b/src/adapters/source-adapters/ChainlinkSourceAdapter.sol index a00dfca..7fb3fae 100644 --- a/src/adapters/source-adapters/ChainlinkSourceAdapter.sol +++ b/src/adapters/source-adapters/ChainlinkSourceAdapter.sol @@ -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 {IAggregatorV3Source} from "../../interfaces/chainlink/IAggregatorV3Source.sol"; import {DiamondRootOval} from "../../DiamondRootOval.sol"; @@ -62,6 +64,18 @@ abstract contract ChainlinkSourceAdapter is DiamondRootOval { return (DecimalLib.convertDecimals(sourceAnswer, SOURCE_DECIMALS, 18), updatedAt); } + /** + * @notice Returns the requested round data from the source. + * @dev If the source does not have the requested round it 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 override returns (int256, uint256) { + (, int256 sourceAnswer,, uint256 updatedAt,) = CHAINLINK_SOURCE.getRoundData(SafeCast.toUint80(roundId)); + return (DecimalLib.convertDecimals(sourceAnswer, SOURCE_DECIMALS, 18), updatedAt); + } + // Tries getting latest data as of requested timestamp. If this is not possible, returns the earliest data available // past the requested timestamp considering the maxTraversal limitations. function _tryLatestRoundDataAt(uint256 timestamp, uint256 maxTraversal) diff --git a/src/adapters/source-adapters/UniswapAnchoredViewSourceAdapter.sol b/src/adapters/source-adapters/UniswapAnchoredViewSourceAdapter.sol index 315e4e4..f1d7f33 100644 --- a/src/adapters/source-adapters/UniswapAnchoredViewSourceAdapter.sol +++ b/src/adapters/source-adapters/UniswapAnchoredViewSourceAdapter.sol @@ -62,6 +62,16 @@ abstract contract UniswapAnchoredViewSourceAdapter is SnapshotSource { return (DecimalLib.convertDecimals(sourcePrice, SOURCE_DECIMALS, 18), latestTimestamp); } + /** + * @notice Returns the requested round data from the source. + * @dev UniswapAnchoredView does not support this and 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 returns (int256, uint256) { + return (0, 0); + } + /** * @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. diff --git a/src/interfaces/chainlink/IAggregatorV3.sol b/src/interfaces/chainlink/IAggregatorV3.sol index 5bb8be5..3c2c6cb 100644 --- a/src/interfaces/chainlink/IAggregatorV3.sol +++ b/src/interfaces/chainlink/IAggregatorV3.sol @@ -13,6 +13,11 @@ interface IAggregatorV3 { view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + function getRoundData(uint80 _roundId) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + // Other Chainlink functions we don't need. // function latestRound() external view returns (uint256); @@ -25,11 +30,6 @@ interface IAggregatorV3 { // function version() external view returns (uint256); - // function getRoundData(uint80 _roundId) - // external - // view - // returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); - // event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); // event NewRound(uint256 indexed roundId, address indexed startedBy, uint256 startedAt); diff --git a/test/fork/adapters/ChainlinkSourceAdapter.sol b/test/fork/adapters/ChainlinkSourceAdapter.sol index ea63dde..3205226 100644 --- a/test/fork/adapters/ChainlinkSourceAdapter.sol +++ b/test/fork/adapters/ChainlinkSourceAdapter.sol @@ -13,6 +13,8 @@ contract TestedSourceAdapter is ChainlinkSourceAdapter { function internalLatestData() public view override returns (int256, uint256, uint256) {} + function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {} + function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {} function lockWindow() public view virtual override returns (uint256) {} diff --git a/test/fork/adapters/UniswapAnchoredViewSourceAdapter.sol b/test/fork/adapters/UniswapAnchoredViewSourceAdapter.sol index e280fd9..45d63e2 100644 --- a/test/fork/adapters/UniswapAnchoredViewSourceAdapter.sol +++ b/test/fork/adapters/UniswapAnchoredViewSourceAdapter.sol @@ -13,6 +13,7 @@ import {IUniswapAnchoredView} from "../../../src/interfaces/compound/IUniswapAnc contract TestedSourceAdapter is UniswapAnchoredViewSourceAdapter { constructor(IUniswapAnchoredView source, address cToken) UniswapAnchoredViewSourceAdapter(source, cToken) {} function internalLatestData() public view override returns (int256, uint256, uint256) {} + function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {} function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {} function lockWindow() public view virtual override returns (uint256) {} function maxTraversal() public view virtual override returns (uint256) {} diff --git a/test/mocks/MockSnapshotSourceAdapter.sol b/test/mocks/MockSnapshotSourceAdapter.sol index ff8a2a7..e4e1e37 100644 --- a/test/mocks/MockSnapshotSourceAdapter.sol +++ b/test/mocks/MockSnapshotSourceAdapter.sol @@ -20,6 +20,10 @@ abstract contract MockSnapshotSourceAdapter is SnapshotSource { return (latestData.answer, latestData.timestamp); } + function getSourceDataAtRound(uint256 /* roundId */ ) public view virtual override returns (int256, uint256) { + return (0, 0); + } + function tryLatestDataAt(uint256 timestamp, uint256 maxTraversal) public view diff --git a/test/mocks/MockSourceAdapter.sol b/test/mocks/MockSourceAdapter.sol index a73dac9..ebba1a5 100644 --- a/test/mocks/MockSourceAdapter.sol +++ b/test/mocks/MockSourceAdapter.sol @@ -40,6 +40,12 @@ abstract contract MockSourceAdapter is DiamondRootOval { return (latestData.answer, latestData.timestamp); } + function getSourceDataAtRound(uint256 roundId) public view virtual override returns (int256, uint256) { + if (roundId == 0 || rounds.length <= roundId) return (0, 0); + RoundData memory roundData = rounds[roundId - 1]; + return (roundData.answer, roundData.timestamp); + } + function _latestRoundData() internal view returns (RoundData memory) { if (rounds.length > 0) return rounds[rounds.length - 1]; return RoundData(0, 0, 0);