Skip to content

Commit

Permalink
fix: convert snapshot source to lib
Browse files Browse the repository at this point in the history
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
  • Loading branch information
Reinis-FRP committed May 16, 2024
1 parent 0fa0033 commit c19424b
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import {DiamondRootOval} from "../../DiamondRootOval.sol";

/**
* @title SnapshotSource contract to be used in conjunction with a source adapter that needs to snapshot historic data.
* @title SnapshotSourceLib library to be used by a source adapter that needs to snapshot historic data.
*/
abstract contract SnapshotSource is DiamondRootOval {
library SnapshotSourceLib {
// Snapshot records the historical answer at a specific timestamp.
struct Snapshot {
int256 answer;
uint256 timestamp;
}

Snapshot[] public snapshots; // Historical answer and timestamp snapshots.

event SnapshotTaken(uint256 snapshotIndex, uint256 indexed timestamp, int256 indexed answer);

/**
* @notice Returns the latest snapshot data.
* @param snapshots Pointer to source adapter's snapshots array.
* @return Snapshot The latest snapshot data.
*/
function latestSnapshotData() public view returns (Snapshot memory) {
function latestSnapshotData(Snapshot[] storage snapshots) internal view returns (Snapshot memory) {
if (snapshots.length > 0) return snapshots[snapshots.length - 1];
return Snapshot(0, 0);
}

/**
* @notice Snapshot the current source data.
* @param snapshots Pointer to source adapter's snapshots array.
* @param latestAnswer The latest answer from the source.
* @param latestTimestamp The timestamp of the latest answer from the source.
*/
function snapshotData() public virtual override {
(int256 answer, uint256 timestamp) = getLatestSourceData();
Snapshot memory snapshot = Snapshot(answer, timestamp);
function snapshotData(Snapshot[] storage snapshots, int256 latestAnswer, uint256 latestTimestamp) internal {
Snapshot memory snapshot = Snapshot(latestAnswer, latestTimestamp);
if (snapshot.timestamp == 0) return; // Should not store invalid data.

// We expect source timestamps to be increasing over time, but there is little we can do to recover if source
Expand All @@ -45,15 +44,20 @@ abstract contract SnapshotSource is DiamondRootOval {
emit SnapshotTaken(snapshotIndex, snapshot.timestamp, snapshot.answer);
}

function _tryLatestDataAt(uint256 timestamp, uint256 maxTraversal) internal view returns (Snapshot memory) {
(int256 answer, uint256 _timestamp) = getLatestSourceData();
Snapshot memory latestData = Snapshot(answer, _timestamp);
function _tryLatestDataAt(
Snapshot[] storage snapshots,
int256 latestAnswer,
uint256 latestTimestamp,
uint256 timestamp,
uint256 maxTraversal
) internal view returns (Snapshot memory) {
Snapshot memory latestData = Snapshot(latestAnswer, latestTimestamp);
// In the happy path there have been no source updates since requested time, so we can return the latest data.
// We can use timestamp property as it matches the block timestamp of the latest source update.
if (latestData.timestamp <= timestamp) return latestData;

// Attempt traversing historical snapshot data. This might still be newer or uninitialized.
Snapshot memory historicalData = _searchSnapshotAt(timestamp, maxTraversal);
Snapshot memory historicalData = _searchSnapshotAt(snapshots, timestamp, maxTraversal);

// Validate returned data. If it is uninitialized we fallback to returning the current latest round data.
if (historicalData.timestamp > 0) return historicalData;
Expand All @@ -62,7 +66,11 @@ abstract contract SnapshotSource is DiamondRootOval {

// Tries finding latest snapshotted data not newer than requested timestamp. Might still return newer data than
// requested if exceeded traversal or hold uninitialized data that should be handled by the caller.
function _searchSnapshotAt(uint256 timestamp, uint256 maxTraversal) internal view returns (Snapshot memory) {
function _searchSnapshotAt(Snapshot[] storage snapshots, uint256 timestamp, uint256 maxTraversal)
internal
view
returns (Snapshot memory)
{
Snapshot memory snapshot;
uint256 traversedSnapshots = 0;
uint256 snapshotId = snapshots.length; // Will decrement when entering loop.
Expand Down
3 changes: 1 addition & 2 deletions src/adapters/source-adapters/BoundedUnionSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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
Expand Down Expand Up @@ -58,7 +57,7 @@ abstract contract BoundedUnionSourceAdapter is
/**
* @notice Snapshots is a no-op for this adapter as its never used.
*/
function snapshotData() public override(ChainlinkSourceAdapter, SnapshotSource) {}
function snapshotData() public override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter) {}

/**
* @notice Tries getting latest data as of requested timestamp. Note that for all historic lookups we simply return
Expand Down
22 changes: 18 additions & 4 deletions src/adapters/source-adapters/ChronicleMedianSourceAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import {SnapshotSource} from "./SnapshotSource.sol";
import {DiamondRootOval} from "../../DiamondRootOval.sol";
import {SnapshotSourceLib} from "../lib/SnapshotSourceLib.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 {
abstract contract ChronicleMedianSourceAdapter is DiamondRootOval {
IMedian public immutable CHRONICLE_SOURCE;

SnapshotSourceLib.Snapshot[] public chronicleMedianSnapshots; // Historical answer and timestamp snapshots.

event SourceSet(address indexed sourceOracle);

constructor(IMedian _chronicleSource) {
Expand All @@ -19,6 +22,14 @@ abstract contract ChronicleMedianSourceAdapter is SnapshotSource {
emit SourceSet(address(_chronicleSource));
}

/**
* @notice Snapshot the current source data.
*/
function snapshotData() public virtual override {
(int256 latestAnswer, uint256 latestTimestamp) = ChronicleMedianSourceAdapter.getLatestSourceData();
SnapshotSourceLib.snapshotData(chronicleMedianSnapshots, latestAnswer, latestTimestamp);
}

/**
* @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
Expand All @@ -33,7 +44,7 @@ abstract contract ChronicleMedianSourceAdapter is SnapshotSource {
/**
* @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.
* @dev Chronicle does not support historical lookups so this uses SnapshotSourceLib 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.
Expand All @@ -46,7 +57,10 @@ abstract contract ChronicleMedianSourceAdapter is SnapshotSource {
override
returns (int256, uint256)
{
Snapshot memory snapshot = _tryLatestDataAt(timestamp, maxTraversal);
(int256 latestAnswer, uint256 latestTimestamp) = ChronicleMedianSourceAdapter.getLatestSourceData();
SnapshotSourceLib.Snapshot memory snapshot = SnapshotSourceLib._tryLatestDataAt(
chronicleMedianSnapshots, latestAnswer, latestTimestamp, timestamp, maxTraversal
);
return (snapshot.answer, snapshot.timestamp);
}
}
21 changes: 17 additions & 4 deletions src/adapters/source-adapters/OSMSourceAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

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

/**
* @title OSMSourceAdapter contract to read data from MakerDAO OSM and standardize it for Oval.
*/
abstract contract OSMSourceAdapter is SnapshotSource {
abstract contract OSMSourceAdapter is DiamondRootOval {
IOSM public immutable osmSource;

// MakerDAO performs decimal conversion in collateral adapter contracts, so all oracle prices are expected to have
// 18 decimals and we can skip decimal conversion.
uint8 public constant decimals = 18;

SnapshotSourceLib.Snapshot[] public osmSnapshots; // Historical answer and timestamp snapshots.

event SourceSet(address indexed sourceOracle);

constructor(IOSM source) {
Expand All @@ -22,6 +25,14 @@ abstract contract OSMSourceAdapter is SnapshotSource {
emit SourceSet(address(source));
}

/**
* @notice Snapshot the current source data.
*/
function snapshotData() public virtual override {
(int256 latestAnswer, uint256 latestTimestamp) = OSMSourceAdapter.getLatestSourceData();
SnapshotSourceLib.snapshotData(osmSnapshots, latestAnswer, latestTimestamp);
}

/**
* @notice Returns the latest data from the source.
* @return answer The latest answer in 18 decimals.
Expand All @@ -34,14 +45,16 @@ abstract contract OSMSourceAdapter is SnapshotSource {
/**
* @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 OSM does not support historical lookups so this uses SnapshotSource to get historic data.
* @dev OSM does not support historical lookups so this uses SnapshotSourceLib 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 override returns (int256, uint256) {
Snapshot memory snapshot = _tryLatestDataAt(timestamp, maxTraversal);
(int256 latestAnswer, uint256 latestTimestamp) = OSMSourceAdapter.getLatestSourceData();
SnapshotSourceLib.Snapshot memory snapshot =
SnapshotSourceLib._tryLatestDataAt(osmSnapshots, latestAnswer, latestTimestamp, timestamp, maxTraversal);
return (snapshot.answer, snapshot.timestamp);
}
}
21 changes: 17 additions & 4 deletions src/adapters/source-adapters/PythSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
pragma solidity 0.8.17;

import {IPyth} from "../../interfaces/pyth/IPyth.sol";
import {SnapshotSource} from "./SnapshotSource.sol";
import {DiamondRootOval} from "../../DiamondRootOval.sol";
import {SnapshotSourceLib} from "../lib/SnapshotSourceLib.sol";
import {DecimalLib} from "../lib/DecimalLib.sol";

/**
* @title PythSourceAdapter contract to read data from Pyth and standardize it for Oval.
*/
abstract contract PythSourceAdapter is SnapshotSource {
abstract contract PythSourceAdapter is DiamondRootOval {
IPyth public immutable PYTH_SOURCE;
bytes32 public immutable PYTH_PRICE_ID;

SnapshotSourceLib.Snapshot[] public pythSnapshots; // Historical answer and timestamp snapshots.

event SourceSet(address indexed sourceOracle, bytes32 indexed pythPriceId);

constructor(IPyth _pyth, bytes32 _pythPriceId) {
Expand All @@ -21,6 +24,14 @@ abstract contract PythSourceAdapter is SnapshotSource {
emit SourceSet(address(_pyth), _pythPriceId);
}

/**
* @notice Snapshot the current source data.
*/
function snapshotData() public virtual override {
(int256 latestAnswer, uint256 latestTimestamp) = PythSourceAdapter.getLatestSourceData();
SnapshotSourceLib.snapshotData(pythSnapshots, latestAnswer, latestTimestamp);
}

/**
* @notice Returns the latest data from the source.
* @return answer The latest answer in 18 decimals.
Expand All @@ -34,7 +45,7 @@ abstract contract PythSourceAdapter is SnapshotSource {
/**
* @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 Pyth does not support historical lookups so this uses SnapshotSource to get historic data.
* @dev Pyth does not support historical lookups so this uses SnapshotSourceLib 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.
Expand All @@ -47,7 +58,9 @@ abstract contract PythSourceAdapter is SnapshotSource {
override
returns (int256, uint256)
{
Snapshot memory snapshot = _tryLatestDataAt(timestamp, maxTraversal);
(int256 latestAnswer, uint256 latestTimestamp) = PythSourceAdapter.getLatestSourceData();
SnapshotSourceLib.Snapshot memory snapshot =
SnapshotSourceLib._tryLatestDataAt(pythSnapshots, latestAnswer, latestTimestamp, timestamp, maxTraversal);
return (snapshot.answer, snapshot.timestamp);
}

Expand Down
6 changes: 3 additions & 3 deletions src/adapters/source-adapters/UnionSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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 UnionSourceAdapter contract to read data from multiple sources and return the newest.
Expand Down Expand Up @@ -45,8 +44,9 @@ abstract contract UnionSourceAdapter is ChainlinkSourceAdapter, ChronicleMedianS
/**
* @notice Snapshots data from all sources that require it.
*/
function snapshotData() public override(ChainlinkSourceAdapter, SnapshotSource) {
SnapshotSource.snapshotData();
function snapshotData() public override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter) {
ChronicleMedianSourceAdapter.snapshotData();
PythSourceAdapter.snapshotData();
}

/**
Expand Down
22 changes: 18 additions & 4 deletions src/adapters/source-adapters/UniswapAnchoredViewSourceAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import {DiamondRootOval} from "../../DiamondRootOval.sol";
import {DecimalLib} from "../lib/DecimalLib.sol";
import {SnapshotSource} from "./SnapshotSource.sol";
import {SnapshotSourceLib} from "../lib/SnapshotSourceLib.sol";
import {IAggregatorV3Source} from "../../interfaces/chainlink/IAggregatorV3Source.sol";
import {IUniswapAnchoredView} from "../../interfaces/compound/IUniswapAnchoredView.sol";
import {IValidatorProxy} from "../../interfaces/compound/IValidatorProxy.sol";
Expand All @@ -11,13 +12,15 @@ import {IValidatorProxy} from "../../interfaces/compound/IValidatorProxy.sol";
* @title UniswapAnchoredViewSourceAdapter contract to read data from UniswapAnchoredView and standardize it for Oval.
*
*/
abstract contract UniswapAnchoredViewSourceAdapter is SnapshotSource {
abstract contract UniswapAnchoredViewSourceAdapter is DiamondRootOval {
IUniswapAnchoredView public immutable UNISWAP_ANCHORED_VIEW;
address public immutable C_TOKEN;
uint8 public immutable SOURCE_DECIMALS;

IAggregatorV3Source public aggregator;

SnapshotSourceLib.Snapshot[] public uniswapAnchoredViewSnapshots; // Historical answer and timestamp snapshots.

event SourceSet(address indexed sourceOracle, address indexed cToken, uint8 indexed sourceDecimals);
event AggregatorSet(address indexed aggregator);

Expand Down Expand Up @@ -51,6 +54,14 @@ abstract contract UniswapAnchoredViewSourceAdapter is SnapshotSource {
emit AggregatorSet(current);
}

/**
* @notice Snapshot the current source data.
*/
function snapshotData() public virtual override {
(int256 latestAnswer, uint256 latestTimestamp) = UniswapAnchoredViewSourceAdapter.getLatestSourceData();
SnapshotSourceLib.snapshotData(uniswapAnchoredViewSnapshots, latestAnswer, latestTimestamp);
}

/**
* @notice Returns the latest data from the source.
* @return answer The latest answer in 18 decimals.
Expand All @@ -65,14 +76,17 @@ abstract contract UniswapAnchoredViewSourceAdapter is SnapshotSource {
/**
* @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 UniswapAnchoredView does not support historical lookups so this uses SnapshotSource to get historic data.
* @dev UniswapAnchoredView does not support historical lookups so this uses SnapshotSourceLib 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 override returns (int256, uint256) {
Snapshot memory snapshot = _tryLatestDataAt(timestamp, maxTraversal);
(int256 latestAnswer, uint256 latestTimestamp) = UniswapAnchoredViewSourceAdapter.getLatestSourceData();
SnapshotSourceLib.Snapshot memory snapshot = SnapshotSourceLib._tryLatestDataAt(
uniswapAnchoredViewSnapshots, latestAnswer, latestTimestamp, timestamp, maxTraversal
);
return (snapshot.answer, snapshot.timestamp);
}
}
Loading

0 comments on commit c19424b

Please sign in to comment.