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

Conversation

Reinis-FRP
Copy link
Collaborator

@Reinis-FRP Reinis-FRP commented May 16, 2024

This test demonstrated that Oval prices can flip backwards when using BoundedUnionSourceAdapter. The root cause of the issue is that stacking multiple source adapters ended up using the same storage slot for snapshotting historical price data.

This PR addresses the issue by converting SnapshotSource to stateless library, so that any source adapters that need snapshotting would instead pass their own storage pointers. With this fixed it now became possible to snapshot individual source oracle data that did not support native historical data.

Added test testLookbackDoesNotFlipBackward demonstrates that now its not possible to return earlier data than returned compared to a request made before.

We might also want to fix this in raw UnionSourceAdapter

Fixes: https://linear.app/uma/issue/UMA-2183/consider-snapshotted-data-in-union-adapters

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Copy link

linear bot commented May 16, 2024

@Reinis-FRP Reinis-FRP changed the title Reinis frp/fix bounded union fix: use snapshotting library in bounded union source adapter May 16, 2024
*/
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?

Copy link
Member

@chrismaree chrismaree left a comment

Choose a reason for hiding this comment

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

Did you run a before and after gas comparison?

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.

@@ -0,0 +1,26 @@
// SPDX-License-Identifier: BUSL-1.1
Copy link
Member

Choose a reason for hiding this comment

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

why is this showing up as new? was this not here before?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we had them only in oval demo repo

Comment on lines 80 to 86
// 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.

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
(int256 crAnswer, uint256 crTimestamp) = ChronicleMedianSourceAdapter.getLatestSourceData();
(int256 pyAnswer, uint256 pyTimestamp) = PythSourceAdapter.getLatestSourceData();
// Chainlink has price history, so use tryLatestDataAt to pull the most recent price that satisfies the timestamp constraint.
(data.clAnswer, data.clTimestamp) = ChainlinkSourceAdapter.tryLatestDataAt(timestamp, maxTraversal);
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 could still be potentially optimized to avoid duplicate calls, but would require introducing version of tryLatestDataAt that accepts already fetched latest data as inputs.

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Copy link
Member

@mrice32 mrice32 left a comment

Choose a reason for hiding this comment

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

LGTM!

if (data.crTimestamp > timestamp) data.crTimestamp = 0;
if (data.pyTimestamp > timestamp) data.pyTimestamp = 0;

// Bounded union prices could have been captured at snapshot that satisfies time constraint.
Copy link
Member

Choose a reason for hiding this comment

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

+1 great comments throughout this file

);

// Return bounded data unless there is a newer snapshotted data that still satisfies time constraint.
if (boundedTimestamp >= snapshot.timestamp || snapshot.timestamp > timestamp) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I've followed the logic correctly, should we have if (boundedTimestamp > snapshot.timestamp) instead, so we give preference to the snapshot data in case there is one?

I understand that at this point (boundedAnswer, boundedTimestamp) and (snapshot.answer, snapshot.timestamp) could be different values if boundedTimestamp == snapshot.timestamp, as the snapshot could have a more complete information?

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 makes sense, pushed the fix

(int256 clAnswer, uint256 clTimestamp,) = ChainlinkSourceAdapter.tryLatestDataAt(timestamp, maxTraversal);
// In the happy path there have been no source updates since requested time, so we can return the latest data.
AllSourceData memory data = _getAllLatestSourceData();
(int256 boundedAnswer, uint256 boundedTimestamp) = _selectBoundedPrice(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we do _selectBoundedPrice(data)

and update _selectBoundedPrice like

    function _selectBoundedPrice(AllSourceData memory data)
        internal
        view
        returns (int256, uint256)
    {

Wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

great suggestion, pushing commit for this

Copy link
Collaborator

@md0x md0x left a comment

Choose a reason for hiding this comment

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

Looks good!
I added one question and one suggestion. Overall, it looks like a solid solution 👍


// To "drop" Chronicle and Pyth, we set their timestamps to 0 (as old as possible) if they are too recent.
// "Drop" Chronicle and/or Pyth by setting their timestamps to 0 (as old as possible) if they are too recent.
Copy link
Member

Choose a reason for hiding this comment

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

the drop logic here I wanted to investigate a bit more:
consider chronicle or pyth have their second most recent values being newer than chainlink, but their newest value is too recent. the implementation below, due to setting these timestamps to zero, will result in this value not being returned, even though the snapshot for these sources might have a "better" value to return (it's newer than chainlink, but still old enough to not be filtered out by the lockWindow).

I might be missing something here on how we want to compare these kinds of sources, but given we now have the ability to snapshot & query all parts of the BoundedUnion can we not consider them equally and look within all sources to find the newest source that is old enough?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we only drop the latest pyth and chronicle values if they are too recent. but we might still end up using any of these sources if they had been selected as part of snapshoted values.

Reinis-FRP and others added 4 commits May 20, 2024 16:06
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
…a point (#14)

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Signed-off-by: chrismaree <christopher.maree@gmail.com>
Co-authored-by: chrismaree <christopher.maree@gmail.com>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Copy link

@daywiss daywiss left a comment

Choose a reason for hiding this comment

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

lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants