Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use snapshotting library in bounded union source adapter #12

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no reason really to snapshot round ID I guess? especially given any source that has the notion of a round ID also has history?

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
18 changes: 11 additions & 7 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 @@ -56,9 +55,12 @@ abstract contract BoundedUnionSourceAdapter is
}

/**
* @notice Snapshots is a no-op for this adapter as its never used.
* @notice Snapshots data from all sources that require it.
*/
function snapshotData() public override(ChainlinkSourceAdapter, SnapshotSource) {}
function snapshotData() public override(ChainlinkSourceAdapter, ChronicleMedianSourceAdapter, PythSourceAdapter) {
ChronicleMedianSourceAdapter.snapshotData();
PythSourceAdapter.snapshotData();
}

/**
* @notice Tries getting latest data as of requested timestamp. Note that for all historic lookups we simply return
Expand All @@ -74,12 +76,14 @@ abstract contract BoundedUnionSourceAdapter is
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.
// Chainlink has native 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();
// For Chronicle and Pyth, tryLatestDataAt would attempt to get price from snapshots, but we can drop them if
// they don't satisfy the timestamp constraint.
(int256 crAnswer, uint256 crTimestamp) = ChronicleMedianSourceAdapter.tryLatestDataAt(timestamp, maxTraversal);
(int256 pyAnswer, uint256 pyTimestamp) = PythSourceAdapter.tryLatestDataAt(timestamp, maxTraversal);
Copy link
Member

@chrismaree chrismaree May 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated. why was it added? it does make sense but given we want the most recent data can we not continue to use those methods?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This attempted to fix the original bug. But now I realize this unnecessarily snapshots source data both in Pyth and Chronicle adapters. Instead, we should snapshot the aggregated union value and use that here - I just refactored this in the latest commit

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes a lot more sence.


// 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.
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice. this is more consistent now.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this works well I think. when you need the snapshot within the adapter you simple: a) import the lib b) have a local snapshots array.


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
Loading