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

feat: add maxAge parameter to limit the staleness of a historical data point (run 2) #16

Merged
merged 2 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/DiamondRootOval.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@ abstract contract DiamondRootOval is IBaseController, IOval, IBaseOracleAdapter
* @notice Time window that bounds how long the permissioned actor has to call the unlockLatestValue function after
* a new source update is posted. If the permissioned actor does not call unlockLatestValue within this window of a
* new source price, the latest value will be made available to everyone without going through an MEV-Share auction.
* @return lockWindow time in seconds.
*/
function lockWindow() public view virtual returns (uint256);

/**
* @notice Max number of historical source updates to traverse when looking for a historic value in the past.
* @return maxTraversal max number of historical source updates to traverse.
*/
function maxTraversal() public view virtual returns (uint256);

/**
* @notice Max age of a historical price that can be used instead of the current price.
*/
function maxAge() public view virtual returns (uint256);
}
4 changes: 3 additions & 1 deletion src/adapters/source-adapters/ChainlinkSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ abstract contract ChainlinkSourceAdapter is DiamondRootOval {
_searchRoundDataAt(timestamp, roundId, maxTraversal);

// Validate returned data. If it is uninitialized we fallback to returning the current latest round data.
if (historicalUpdatedAt > 0) return (historicalAnswer, historicalUpdatedAt, historicalRoundId);
if (historicalUpdatedAt > block.timestamp - maxAge()) {
return (historicalAnswer, historicalUpdatedAt, historicalRoundId);
}
return (answer, updatedAt, roundId);
}

Expand Down
4 changes: 2 additions & 2 deletions src/adapters/source-adapters/SnapshotSource.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ abstract contract SnapshotSource is DiamondRootOval {
// Attempt traversing historical snapshot data. This might still be newer or uninitialized.
Snapshot memory historicalData = _searchSnapshotAt(timestamp, maxTraversal);

// Validate returned data. If it is uninitialized we fallback to returning the current latest round data.
if (historicalData.timestamp > 0) return historicalData;
// Validate returned data. If it is uninitialized or too old we fallback to returning the current latest round data.
if (historicalData.timestamp >= block.timestamp - maxAge()) return historicalData;
return latestData;
}

Expand Down
18 changes: 18 additions & 0 deletions src/controllers/BaseController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ abstract contract BaseController is Ownable, Oval {
// these don't need to be public since they can be accessed via the accessor functions below.
uint256 private lockWindow_ = 60; // The lockWindow in seconds.
uint256 private maxTraversal_ = 10; // The maximum number of rounds to traverse when looking for historical data.
uint256 private maxAge_ = 1 days; // Default 1 day.

mapping(address => bool) public unlockers;

Expand Down Expand Up @@ -66,6 +67,16 @@ abstract contract BaseController is Ownable, Oval {
emit MaxTraversalSet(newMaxTraversal);
}

/**
* @notice Enables the owner to set the maxAge.
* @param newMaxAge The maxAge to set
*/
function setMaxAge(uint256 newMaxAge) public onlyOwner {
maxAge_ = newMaxAge;

emit MaxAgeSet(newMaxAge);
}

/**
* @notice Time window that bounds how long the permissioned actor has to call the unlockLatestValue function after
* a new source update is posted. If the permissioned actor does not call unlockLatestValue within this window of a
Expand All @@ -83,4 +94,11 @@ abstract contract BaseController is Ownable, Oval {
function maxTraversal() public view override returns (uint256) {
return maxTraversal_;
}

/**
* @notice Max age of a historical price that can be used instead of the current price.
*/
function maxAge() public view override returns (uint256) {
return maxAge_;
}
}
12 changes: 11 additions & 1 deletion src/controllers/ImmutableController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {Oval} from "../Oval.sol";
abstract contract ImmutableController is Oval {
uint256 private immutable LOCK_WINDOW; // The lockWindow in seconds.
uint256 private immutable MAX_TRAVERSAL; // The maximum number of rounds to traverse when looking for historical data.
uint256 private immutable MAX_AGE;

mapping(address => bool) public unlockers;

constructor(uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers) {
constructor(uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers, uint256 _maxAge) {
LOCK_WINDOW = _lockWindow;
MAX_TRAVERSAL = _maxTraversal;
MAX_AGE = _maxAge;
for (uint256 i = 0; i < _unlockers.length; i++) {
unlockers[_unlockers[i]] = true;

Expand All @@ -27,6 +29,7 @@ abstract contract ImmutableController is Oval {

emit LockWindowSet(_lockWindow);
emit MaxTraversalSet(_maxTraversal);
emit MaxAgeSet(_maxAge);
}

/**
Expand Down Expand Up @@ -57,4 +60,11 @@ abstract contract ImmutableController is Oval {
function maxTraversal() public view override returns (uint256) {
return MAX_TRAVERSAL;
}

/**
* @notice Max age of a historical price that can be used instead of the current price.
*/
function maxAge() public view override returns (uint256) {
return MAX_AGE;
}
}
13 changes: 12 additions & 1 deletion src/controllers/MutableUnlockersController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ abstract contract MutableUnlockersController is Ownable, Oval {
// these don't need to be public since they can be accessed via the accessor functions below.
uint256 private immutable LOCK_WINDOW; // The lockWindow in seconds.
uint256 private immutable MAX_TRAVERSAL; // The maximum number of rounds to traverse when looking for historical data.
uint256 private immutable MAX_AGE; // Max age for a historical price used by Oval instead of the current price.

mapping(address => bool) public unlockers;

constructor(uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers) {
constructor(uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers, uint256 _maxAge) {
LOCK_WINDOW = _lockWindow;
MAX_TRAVERSAL = _maxTraversal;
MAX_AGE = _maxAge;

for (uint256 i = 0; i < _unlockers.length; i++) {
setUnlocker(_unlockers[i], true);
}

emit LockWindowSet(_lockWindow);
emit MaxTraversalSet(_maxTraversal);
emit MaxAgeSet(_maxAge);
}

/**
Expand Down Expand Up @@ -65,4 +69,11 @@ abstract contract MutableUnlockersController is Ownable, Oval {
function maxTraversal() public view override returns (uint256) {
return MAX_TRAVERSAL;
}

/**
* @notice Max age of a historical price that can be used instead of the current price.
*/
function maxAge() public view override returns (uint256) {
return MAX_AGE;
}
}
9 changes: 6 additions & 3 deletions src/factories/StandardChainlinkFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ contract OvalChainlink is MutableUnlockersController, ChainlinkSourceAdapter, Ch
address[] memory unlockers,
uint256 lockWindow,
uint256 maxTraversal,
uint256 maxAge,
address owner
)
ChainlinkSourceAdapter(source)
MutableUnlockersController(lockWindow, maxTraversal, unlockers)
MutableUnlockersController(lockWindow, maxTraversal, unlockers, maxAge)
ChainlinkDestinationAdapter(18)
{
_transferOwnership(owner);
Expand All @@ -44,10 +45,12 @@ contract StandardChainlinkFactory is Ownable, BaseFactory {
* @param source the Chainlink oracle source contract.
* @param lockWindow the lockWindow used for this Oval instance. This is the length of the window
* for the Oval auction to be run and, thus, the maximum time that prices will be delayed.
* @param maxAge max age of a price that is used in place of the current price. If the only available price is
* older than this, OEV is not captured and the current price is provided.
* @return oval deployed oval address.
*/
function create(IAggregatorV3Source source, uint256 lockWindow) external returns (address oval) {
oval = address(new OvalChainlink(source, defaultUnlockers, lockWindow, MAX_TRAVERSAL, owner()));
function create(IAggregatorV3Source source, uint256 lockWindow, uint256 maxAge) external returns (address oval) {
oval = address(new OvalChainlink(source, defaultUnlockers, lockWindow, MAX_TRAVERSAL, maxAge, owner()));
emit OvalDeployed(msg.sender, oval, lockWindow, MAX_TRAVERSAL, owner(), defaultUnlockers);
}
}
9 changes: 6 additions & 3 deletions src/factories/StandardChronicleFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ contract OvalChronicle is MutableUnlockersController, ChronicleMedianSourceAdapt
address[] memory _unlockers,
uint256 _lockWindow,
uint256 _maxTraversal,
uint256 _maxAge,
address _owner
)
ChronicleMedianSourceAdapter(_source)
MutableUnlockersController(_lockWindow, _maxTraversal, _unlockers)
MutableUnlockersController(_lockWindow, _maxTraversal, _unlockers, _maxAge)
ChainlinkDestinationAdapter(18)
{
_transferOwnership(_owner);
Expand All @@ -45,10 +46,12 @@ contract StandardChronicleFactory is Ownable, BaseFactory {
* @param chronicle Chronicle source contract.
* @param lockWindow the lockWindow used for this Oval instance. This is the length of the window
* for the Oval auction to be run and, thus, the maximum time that prices will be delayed.
* @param maxAge max age of a price that is used in place of the current price. If the only available price is
* older than this, OEV is not captured and the current price is provided.
* @return oval deployed oval address.
*/
function create(IMedian chronicle, uint256 lockWindow) external returns (address oval) {
oval = address(new OvalChronicle(chronicle, defaultUnlockers, lockWindow, MAX_TRAVERSAL, owner()));
function create(IMedian chronicle, uint256 lockWindow, uint256 maxAge) external returns (address oval) {
oval = address(new OvalChronicle(chronicle, defaultUnlockers, lockWindow, MAX_TRAVERSAL, maxAge, owner()));
emit OvalDeployed(msg.sender, oval, lockWindow, MAX_TRAVERSAL, owner(), defaultUnlockers);
}
}
9 changes: 6 additions & 3 deletions src/factories/StandardCoinbaseFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ contract OvalCoinbase is MutableUnlockersController, CoinbaseSourceAdapter, Chai
address[] memory _unlockers,
uint256 _lockWindow,
uint256 _maxTraversal,
uint256 _maxAge,
address _owner
)
CoinbaseSourceAdapter(_source, _ticker)
MutableUnlockersController(_lockWindow, _maxTraversal, _unlockers)
MutableUnlockersController(_lockWindow, _maxTraversal, _unlockers, _maxAge)
ChainlinkDestinationAdapter(18)
{
_transferOwnership(_owner);
Expand All @@ -49,10 +50,12 @@ contract StandardCoinbaseFactory is Ownable, BaseFactory {
* @param ticker the Coinbase oracle's ticker.
* @param lockWindow the lockWindow used for this Oval instance. This is the length of the window
* for the Oval auction to be run and, thus, the maximum time that prices will be delayed.
* @param maxAge max age of a price that is used in place of the current price. If the only available price is
* older than this, OEV is not captured and the current price is provided.
* @return oval deployed oval address.
*/
function create(string memory ticker, uint256 lockWindow) external returns (address oval) {
oval = address(new OvalCoinbase(SOURCE, ticker, defaultUnlockers, lockWindow, MAX_TRAVERSAL, owner()));
function create(string memory ticker, uint256 lockWindow, uint256 maxAge) external returns (address oval) {
oval = address(new OvalCoinbase(SOURCE, ticker, defaultUnlockers, lockWindow, MAX_TRAVERSAL, maxAge, owner()));
emit OvalDeployed(msg.sender, oval, lockWindow, MAX_TRAVERSAL, owner(), defaultUnlockers);
}
}
9 changes: 6 additions & 3 deletions src/factories/StandardPythFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ contract OvalPyth is MutableUnlockersController, PythSourceAdapter, ChainlinkDes
address[] memory unlockers,
uint256 lockWindow,
uint256 maxTraversal,
uint256 maxAge,
address owner
)
PythSourceAdapter(source, pythPriceId)
MutableUnlockersController(lockWindow, maxTraversal, unlockers)
MutableUnlockersController(lockWindow, maxTraversal, unlockers, maxAge)
ChainlinkDestinationAdapter(18)
{
_transferOwnership(owner);
Expand All @@ -50,10 +51,12 @@ contract StandardPythFactory is Ownable, BaseFactory {
* @param pythPriceId the Pyth price id.
* @param lockWindow the lockWindow used for this Oval instance. This is the length of the window
* for the Oval auction to be run and, thus, the maximum time that prices will be delayed.
* @param maxAge max age of a price that is used in place of the current price. If the only available price is
* older than this, OEV is not captured and the current price is provided.
* @return oval deployed oval address.
*/
function create(bytes32 pythPriceId, uint256 lockWindow) external returns (address oval) {
oval = address(new OvalPyth(pyth, pythPriceId, defaultUnlockers, lockWindow, MAX_TRAVERSAL, owner()));
function create(bytes32 pythPriceId, uint256 lockWindow, uint256 maxAge) external returns (address oval) {
oval = address(new OvalPyth(pyth, pythPriceId, defaultUnlockers, lockWindow, MAX_TRAVERSAL, maxAge, owner()));
emit OvalDeployed(msg.sender, oval, lockWindow, MAX_TRAVERSAL, owner(), defaultUnlockers);
}
}
1 change: 1 addition & 0 deletions src/interfaces/IBaseController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface IBaseController {
event LockWindowSet(uint256 indexed lockWindow);
event MaxTraversalSet(uint256 indexed maxTraversal);
event UnlockerSet(address indexed unlocker, bool indexed allowed);
event MaxAgeSet(uint256 indexed newMaxAge);

function canUnlock(address caller, uint256 cachedLatestTimestamp) external view returns (bool);
}
2 changes: 1 addition & 1 deletion test/fork/aave/AaveV2.Liquidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Usdc is IERC20 {
contract TestedOval is ImmutableController, ChainlinkSourceAdapter, ChainlinkDestinationAdapter {
constructor(IAggregatorV3Source source, uint8 decimals, address[] memory unlockers)
ChainlinkSourceAdapter(source)
ImmutableController(60, 10, unlockers)
ImmutableController(60, 10, unlockers, 86400)
ChainlinkDestinationAdapter(decimals)
{}
}
Expand Down
2 changes: 1 addition & 1 deletion test/fork/aave/AaveV3.Liquidation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Usdc is IERC20 {
contract TestedOval is ImmutableController, ChainlinkSourceAdapter, ChainlinkDestinationAdapter {
constructor(IAggregatorV3Source source, uint8 decimals, address[] memory unlockers)
ChainlinkSourceAdapter(source)
ImmutableController(60, 10, unlockers)
ImmutableController(60, 10, unlockers, 86400)
ChainlinkDestinationAdapter(decimals)
{}
}
Expand Down
39 changes: 28 additions & 11 deletions test/fork/adapters/ChainlinkSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,8 @@ import {ChainlinkSourceAdapter} from "../../../src/adapters/source-adapters/Chai
import {DecimalLib} from "../../../src/adapters/lib/DecimalLib.sol";
import {IAggregatorV3Source} from "../../../src/interfaces/chainlink/IAggregatorV3Source.sol";

contract TestedSourceAdapter is ChainlinkSourceAdapter {
contract TestedSourceAdapter is ChainlinkSourceAdapter, BaseController {
constructor(IAggregatorV3Source source) ChainlinkSourceAdapter(source) {}

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) {}
}

contract ChainlinkSourceAdapterTest is CommonTest {
Expand Down Expand Up @@ -107,6 +97,33 @@ contract ChainlinkSourceAdapterTest is CommonTest {
assertTrue(uint256(roundId) == lookBackRoundId);
}

function testCorrectlyBoundsMaxLooBackByMaxAge() public {
// Value returned at 2 days should be the same as the value returned at 1 day as the max age is 1 day.
assertTrue(sourceAdapter.maxAge() == 1 days);
(int256 lookBackPricePastWindow, uint256 lookBackTimestampPastWindow, uint256 lookBackRoundIdPastWindow) =
sourceAdapter.tryLatestDataAt(block.timestamp - 2 days, 50);

(int256 lookBackPriceAtLimit, uint256 lookBackTimestampAtLimit, uint256 lookBackRoundIdAtLimit) =
sourceAdapter.tryLatestDataAt(block.timestamp - 1 days, 50);

assertTrue(lookBackPricePastWindow == lookBackPriceAtLimit);
assertTrue(lookBackTimestampPastWindow == lookBackTimestampAtLimit);
assertTrue(lookBackRoundIdPastWindow == lookBackRoundIdAtLimit);
}

function testExtendingMaxAgeCorrectlyExtendsWindowOfReturnedValue() public {
sourceAdapter.setMaxAge(2 days);
(int256 lookBackPricePastWindow, uint256 lookBackTimestampPastWindow, uint256 lookBackRoundIdPastWindow) =
sourceAdapter.tryLatestDataAt(block.timestamp - 3 days, 50);

(int256 lookBackPriceAtLimit, uint256 lookBackTimestampAtLimit, uint256 lookBackRoundIdAtLimit) =
sourceAdapter.tryLatestDataAt(block.timestamp - 2 days, 50);

assertTrue(lookBackPricePastWindow == lookBackPriceAtLimit);
assertTrue(lookBackTimestampPastWindow == lookBackTimestampAtLimit);
assertTrue(lookBackRoundIdPastWindow == lookBackRoundIdAtLimit);
}

function testNonHistoricalData() public {
uint256 targetTime = block.timestamp - 1 hours;

Expand Down
8 changes: 2 additions & 6 deletions test/fork/adapters/ChronicleMedianSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@ pragma solidity 0.8.17;

import {CommonTest} from "../../Common.sol";

import {BaseController} from "../../../src/controllers/BaseController.sol";
import {ChronicleMedianSourceAdapter} from "../../../src/adapters/source-adapters/ChronicleMedianSourceAdapter.sol";
import {IMedian} from "../../../src/interfaces/chronicle/IMedian.sol";

contract TestedSourceAdapter is ChronicleMedianSourceAdapter {
contract TestedSourceAdapter is ChronicleMedianSourceAdapter, BaseController {
constructor(IMedian source) ChronicleMedianSourceAdapter(source) {}
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) {}
}

contract ChronicleMedianSourceAdapterTest is CommonTest {
Expand Down
8 changes: 2 additions & 6 deletions test/fork/adapters/OSMSourceAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@ pragma solidity 0.8.17;

import {CommonTest} from "../../Common.sol";

import {BaseController} from "../../../src/controllers/BaseController.sol";
import {OSMSourceAdapter} from "../../../src/adapters/source-adapters/OSMSourceAdapter.sol";
import {IOSM} from "../../../src/interfaces/makerdao/IOSM.sol";
import {IMedian} from "../../../src/interfaces/chronicle/IMedian.sol";

contract TestedSourceAdapter is OSMSourceAdapter {
contract TestedSourceAdapter is OSMSourceAdapter, BaseController {
constructor(IOSM source) OSMSourceAdapter(source) {}
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) {}
}

contract OSMSourceAdapterTest is CommonTest {
Expand Down
Loading
Loading