Skip to content

Commit

Permalink
feat: add maxAge parameter to limit the staleness of a historical dat…
Browse files Browse the repository at this point in the history
…a point (run 2) (#16)

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Signed-off-by: chrismaree <christopher.maree@gmail.com>
Co-authored-by: chrismaree <christopher.maree@gmail.com>
  • Loading branch information
mrice32 and chrismaree committed May 20, 2024
1 parent c98863e commit 297ab25
Show file tree
Hide file tree
Showing 32 changed files with 196 additions and 108 deletions.
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

0 comments on commit 297ab25

Please sign in to comment.