Skip to content

Commit 424d94e

Browse files
mrice32chrismaree
andauthored
feat: add maxAge parameter to limit the staleness of a historical data 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>
1 parent c98863e commit 424d94e

19 files changed

+145
-63
lines changed

src/DiamondRootOval.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,16 @@ abstract contract DiamondRootOval is IBaseController, IOval, IBaseOracleAdapter
7575
* @notice Time window that bounds how long the permissioned actor has to call the unlockLatestValue function after
7676
* a new source update is posted. If the permissioned actor does not call unlockLatestValue within this window of a
7777
* new source price, the latest value will be made available to everyone without going through an MEV-Share auction.
78-
* @return lockWindow time in seconds.
7978
*/
8079
function lockWindow() public view virtual returns (uint256);
8180

8281
/**
8382
* @notice Max number of historical source updates to traverse when looking for a historic value in the past.
84-
* @return maxTraversal max number of historical source updates to traverse.
8583
*/
8684
function maxTraversal() public view virtual returns (uint256);
85+
86+
/**
87+
* @notice Max age of a historical price that can be used instead of the current price.
88+
*/
89+
function maxAge() public view virtual returns (uint256);
8790
}

src/adapters/source-adapters/ChainlinkSourceAdapter.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ abstract contract ChainlinkSourceAdapter is DiamondRootOval {
9494
_searchRoundDataAt(timestamp, roundId, maxTraversal);
9595

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

src/adapters/source-adapters/SnapshotSource.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ abstract contract SnapshotSource is DiamondRootOval {
5555
// Attempt traversing historical snapshot data. This might still be newer or uninitialized.
5656
Snapshot memory historicalData = _searchSnapshotAt(timestamp, maxTraversal);
5757

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

src/controllers/BaseController.sol

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

1617
mapping(address => bool) public unlockers;
1718

@@ -66,6 +67,16 @@ abstract contract BaseController is Ownable, Oval {
6667
emit MaxTraversalSet(newMaxTraversal);
6768
}
6869

70+
/**
71+
* @notice Enables the owner to set the maxAge.
72+
* @param newMaxAge The maxAge to set
73+
*/
74+
function setMaxAge(uint256 newMaxAge) public onlyOwner {
75+
maxAge_ = newMaxAge;
76+
77+
emit MaxAgeSet(newMaxAge);
78+
}
79+
6980
/**
7081
* @notice Time window that bounds how long the permissioned actor has to call the unlockLatestValue function after
7182
* a new source update is posted. If the permissioned actor does not call unlockLatestValue within this window of a
@@ -83,4 +94,11 @@ abstract contract BaseController is Ownable, Oval {
8394
function maxTraversal() public view override returns (uint256) {
8495
return maxTraversal_;
8596
}
97+
98+
/**
99+
* @notice Max age of a historical price that can be used instead of the current price.
100+
*/
101+
function maxAge() public view override returns (uint256) {
102+
return maxAge_;
103+
}
86104
}

src/controllers/ImmutableController.sol

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {Oval} from "../Oval.sol";
1313
abstract contract ImmutableController is Oval {
1414
uint256 private immutable LOCK_WINDOW; // The lockWindow in seconds.
1515
uint256 private immutable MAX_TRAVERSAL; // The maximum number of rounds to traverse when looking for historical data.
16+
uint256 private immutable MAX_AGE;
1617

1718
mapping(address => bool) public unlockers;
1819

19-
constructor(uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers) {
20+
constructor(uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers, uint256 _maxAge) {
2021
LOCK_WINDOW = _lockWindow;
2122
MAX_TRAVERSAL = _maxTraversal;
23+
MAX_AGE = _maxAge;
2224
for (uint256 i = 0; i < _unlockers.length; i++) {
2325
unlockers[_unlockers[i]] = true;
2426

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

2830
emit LockWindowSet(_lockWindow);
2931
emit MaxTraversalSet(_maxTraversal);
32+
emit MaxAgeSet(_maxAge);
3033
}
3134

3235
/**
@@ -57,4 +60,11 @@ abstract contract ImmutableController is Oval {
5760
function maxTraversal() public view override returns (uint256) {
5861
return MAX_TRAVERSAL;
5962
}
63+
64+
/**
65+
* @notice Max age of a historical price that can be used instead of the current price.
66+
*/
67+
function maxAge() public view override returns (uint256) {
68+
return MAX_AGE;
69+
}
6070
}

src/interfaces/IBaseController.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface IBaseController {
55
event LockWindowSet(uint256 indexed lockWindow);
66
event MaxTraversalSet(uint256 indexed maxTraversal);
77
event UnlockerSet(address indexed unlocker, bool indexed allowed);
8+
event MaxAgeSet(uint256 indexed newMaxAge);
89

910
function canUnlock(address caller, uint256 cachedLatestTimestamp) external view returns (bool);
1011
}

test/fork/aave/AaveV2.Liquidation.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface Usdc is IERC20 {
2020
contract TestedOval is ImmutableController, ChainlinkSourceAdapter, ChainlinkDestinationAdapter {
2121
constructor(IAggregatorV3Source source, uint8 decimals, address[] memory unlockers)
2222
ChainlinkSourceAdapter(source)
23-
ImmutableController(60, 10, unlockers)
23+
ImmutableController(60, 10, unlockers, 86400)
2424
ChainlinkDestinationAdapter(decimals)
2525
{}
2626
}

test/fork/aave/AaveV3.Liquidation.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface Usdc is IERC20 {
2020
contract TestedOval is ImmutableController, ChainlinkSourceAdapter, ChainlinkDestinationAdapter {
2121
constructor(IAggregatorV3Source source, uint8 decimals, address[] memory unlockers)
2222
ChainlinkSourceAdapter(source)
23-
ImmutableController(60, 10, unlockers)
23+
ImmutableController(60, 10, unlockers, 86400)
2424
ChainlinkDestinationAdapter(decimals)
2525
{}
2626
}

test/fork/adapters/ChainlinkSourceAdapter.sol

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,8 @@ import {ChainlinkSourceAdapter} from "../../../src/adapters/source-adapters/Chai
88
import {DecimalLib} from "../../../src/adapters/lib/DecimalLib.sol";
99
import {IAggregatorV3Source} from "../../../src/interfaces/chainlink/IAggregatorV3Source.sol";
1010

11-
contract TestedSourceAdapter is ChainlinkSourceAdapter {
11+
contract TestedSourceAdapter is ChainlinkSourceAdapter, BaseController {
1212
constructor(IAggregatorV3Source source) ChainlinkSourceAdapter(source) {}
13-
14-
function internalLatestData() public view override returns (int256, uint256, uint256) {}
15-
16-
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {}
17-
18-
function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {}
19-
20-
function lockWindow() public view virtual override returns (uint256) {}
21-
22-
function maxTraversal() public view virtual override returns (uint256) {}
2313
}
2414

2515
contract ChainlinkSourceAdapterTest is CommonTest {
@@ -107,6 +97,33 @@ contract ChainlinkSourceAdapterTest is CommonTest {
10797
assertTrue(uint256(roundId) == lookBackRoundId);
10898
}
10999

100+
function testCorrectlyBoundsMaxLooBackByMaxAge() public {
101+
// Value returned at 2 days should be the same as the value returned at 1 day as the max age is 1 day.
102+
assertTrue(sourceAdapter.maxAge() == 1 days);
103+
(int256 lookBackPricePastWindow, uint256 lookBackTimestampPastWindow, uint256 lookBackRoundIdPastWindow) =
104+
sourceAdapter.tryLatestDataAt(block.timestamp - 2 days, 50);
105+
106+
(int256 lookBackPriceAtLimit, uint256 lookBackTimestampAtLimit, uint256 lookBackRoundIdAtLimit) =
107+
sourceAdapter.tryLatestDataAt(block.timestamp - 1 days, 50);
108+
109+
assertTrue(lookBackPricePastWindow == lookBackPriceAtLimit);
110+
assertTrue(lookBackTimestampPastWindow == lookBackTimestampAtLimit);
111+
assertTrue(lookBackRoundIdPastWindow == lookBackRoundIdAtLimit);
112+
}
113+
114+
function testExtendingMaxAgeCorrectlyExtendsWindowOfReturnedValue() public {
115+
sourceAdapter.setMaxAge(2 days);
116+
(int256 lookBackPricePastWindow, uint256 lookBackTimestampPastWindow, uint256 lookBackRoundIdPastWindow) =
117+
sourceAdapter.tryLatestDataAt(block.timestamp - 3 days, 50);
118+
119+
(int256 lookBackPriceAtLimit, uint256 lookBackTimestampAtLimit, uint256 lookBackRoundIdAtLimit) =
120+
sourceAdapter.tryLatestDataAt(block.timestamp - 2 days, 50);
121+
122+
assertTrue(lookBackPricePastWindow == lookBackPriceAtLimit);
123+
assertTrue(lookBackTimestampPastWindow == lookBackTimestampAtLimit);
124+
assertTrue(lookBackRoundIdPastWindow == lookBackRoundIdAtLimit);
125+
}
126+
110127
function testNonHistoricalData() public {
111128
uint256 targetTime = block.timestamp - 1 hours;
112129

test/fork/adapters/ChronicleMedianSourceAdapter.sol

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ pragma solidity 0.8.17;
33

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

6+
import {BaseController} from "../../../src/controllers/BaseController.sol";
67
import {ChronicleMedianSourceAdapter} from "../../../src/adapters/source-adapters/ChronicleMedianSourceAdapter.sol";
78
import {IMedian} from "../../../src/interfaces/chronicle/IMedian.sol";
89

9-
contract TestedSourceAdapter is ChronicleMedianSourceAdapter {
10+
contract TestedSourceAdapter is ChronicleMedianSourceAdapter, BaseController {
1011
constructor(IMedian source) ChronicleMedianSourceAdapter(source) {}
11-
function internalLatestData() public view override returns (int256, uint256, uint256) {}
12-
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {}
13-
function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {}
14-
function lockWindow() public view virtual override returns (uint256) {}
15-
function maxTraversal() public view virtual override returns (uint256) {}
1612
}
1713

1814
contract ChronicleMedianSourceAdapterTest is CommonTest {

test/fork/adapters/OSMSourceAdapter.sol

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@ pragma solidity 0.8.17;
33

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

6+
import {BaseController} from "../../../src/controllers/BaseController.sol";
67
import {OSMSourceAdapter} from "../../../src/adapters/source-adapters/OSMSourceAdapter.sol";
78
import {IOSM} from "../../../src/interfaces/makerdao/IOSM.sol";
89
import {IMedian} from "../../../src/interfaces/chronicle/IMedian.sol";
910

10-
contract TestedSourceAdapter is OSMSourceAdapter {
11+
contract TestedSourceAdapter is OSMSourceAdapter, BaseController {
1112
constructor(IOSM source) OSMSourceAdapter(source) {}
12-
function internalLatestData() public view override returns (int256, uint256, uint256) {}
13-
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {}
14-
function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {}
15-
function lockWindow() public view virtual override returns (uint256) {}
16-
function maxTraversal() public view virtual override returns (uint256) {}
1713
}
1814

1915
contract OSMSourceAdapterTest is CommonTest {

test/fork/adapters/PythSourceAdapter.sol

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,12 @@ pragma solidity 0.8.17;
33

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

6+
import {BaseController} from "../../../src/controllers/BaseController.sol";
67
import {PythSourceAdapter} from "../../../src/adapters/source-adapters/PythSourceAdapter.sol";
78
import {IPyth} from "../../../src/interfaces/pyth/IPyth.sol";
89

9-
contract TestedSourceAdapter is PythSourceAdapter {
10+
contract TestedSourceAdapter is PythSourceAdapter, BaseController {
1011
constructor(IPyth source, bytes32 priceId) PythSourceAdapter(source, priceId) {}
11-
12-
function internalLatestData() public view override returns (int256, uint256, uint256) {}
13-
14-
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {}
15-
16-
function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {}
17-
18-
function lockWindow() public view virtual override returns (uint256) {}
19-
20-
function maxTraversal() public view virtual override returns (uint256) {}
2112
}
2213

2314
contract PythSourceAdapterTest is CommonTest {

test/fork/adapters/UnionSourceAdapter.sol

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,18 @@
22
pragma solidity 0.8.17;
33

44
import {CommonTest} from "../../Common.sol";
5+
6+
import {BaseController} from "../../../src/controllers/BaseController.sol";
57
import {UnionSourceAdapter} from "../../../src/adapters/source-adapters/UnionSourceAdapter.sol";
68
import {IAggregatorV3Source} from "../../../src/interfaces/chainlink/IAggregatorV3Source.sol";
79
import {IMedian} from "../../../src/interfaces/chronicle/IMedian.sol";
810
import {IPyth} from "../../../src/interfaces/pyth/IPyth.sol";
911
import {DecimalLib} from "../../../src/adapters/lib/DecimalLib.sol";
1012

11-
contract TestedSourceAdapter is UnionSourceAdapter {
13+
contract TestedSourceAdapter is UnionSourceAdapter, BaseController {
1214
constructor(IAggregatorV3Source chainlink, IMedian chronicle, IPyth pyth, bytes32 pythPriceId)
1315
UnionSourceAdapter(chainlink, chronicle, pyth, pythPriceId)
1416
{}
15-
16-
function internalLatestData() public view override returns (int256, uint256, uint256) {}
17-
18-
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {}
19-
20-
function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {}
21-
22-
function lockWindow() public view virtual override returns (uint256) {}
23-
24-
function maxTraversal() public view virtual override returns (uint256) {}
2517
}
2618

2719
contract UnionSourceAdapterTest is CommonTest {
@@ -174,6 +166,7 @@ contract UnionSourceAdapterTest is CommonTest {
174166
// Fork to a block where chronicle was the newest.
175167
vm.createSelectFork("mainnet", targetChronicleBlock);
176168
uint256 targetTimestamp = block.timestamp;
169+
sourceAdapter.setMaxAge(2 days); // Set max age to 2 days to disable this logic for the test.
177170
_whitelistOnChronicle();
178171

179172
// Snapshotting union adapter should not affect historical lookups, but we do it just to prove it does not interfere.

test/fork/adapters/UniswapAnchoredViewSourceAdapter.sol

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,16 @@ pragma solidity 0.8.17;
33

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

6+
import {BaseController} from "../../../src/controllers/BaseController.sol";
67
import {IValidatorProxyTest} from "../interfaces/compoundV2/IValidatorProxy.sol";
78
import {MockChainlinkV3Aggregator} from "../../mocks/MockChainlinkV3Aggregator.sol";
89
import {UniswapAnchoredViewSourceAdapter} from
910
"../../../src/adapters/source-adapters/UniswapAnchoredViewSourceAdapter.sol";
1011
import {IAccessControlledAggregatorV3} from "../../../src/interfaces/chainlink/IAccessControlledAggregatorV3.sol";
1112
import {IUniswapAnchoredView} from "../../../src/interfaces/compound/IUniswapAnchoredView.sol";
1213

13-
contract TestedSourceAdapter is UniswapAnchoredViewSourceAdapter {
14+
contract TestedSourceAdapter is UniswapAnchoredViewSourceAdapter, BaseController {
1415
constructor(IUniswapAnchoredView source, address cToken) UniswapAnchoredViewSourceAdapter(source, cToken) {}
15-
function internalLatestData() public view override returns (int256, uint256, uint256) {}
16-
function internalDataAtRound(uint256 roundId) public view override returns (int256, uint256) {}
17-
function canUnlock(address caller, uint256 cachedLatestTimestamp) public view virtual override returns (bool) {}
18-
function lockWindow() public view virtual override returns (uint256) {}
19-
function maxTraversal() public view virtual override returns (uint256) {}
2016
}
2117

2218
contract UniswapAnchoredViewSourceAdapterTest is CommonTest {

test/fork/compound/CompoundV2.Liquidation.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface Usdc is IERC20 {
2727
contract TestedOval is ImmutableController, UniswapAnchoredViewSourceAdapter, BaseDestinationAdapter {
2828
constructor(IUniswapAnchoredView source, address cToken, address[] memory unlockers)
2929
UniswapAnchoredViewSourceAdapter(source, cToken)
30-
ImmutableController(60, 10, unlockers)
30+
ImmutableController(60, 10, unlockers, 86400)
3131
BaseDestinationAdapter()
3232
{}
3333
}

test/unit/BaseController.sol

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,17 @@ contract BaseControllerTest is CommonTest {
6565
vm.expectRevert("Ownable: caller is not the owner");
6666
baseController.setMaxTraversal(100);
6767
}
68+
69+
function testOwnerCanSetMaxAge() public {
70+
uint256 newMaxAge = 7200; // 2 hours in seconds, different from default 1 day
71+
vm.prank(owner);
72+
baseController.setMaxAge(newMaxAge);
73+
assertTrue(baseController.maxAge() == newMaxAge);
74+
}
75+
76+
function testNonOwnerCannotSetMaxAge() public {
77+
vm.prank(random);
78+
vm.expectRevert("Ownable: caller is not the owner");
79+
baseController.setMaxAge(7200);
80+
}
6881
}

test/unit/ImmutableController.sol

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import {BaseDestinationAdapter} from "../../src/adapters/destination-adapters/Ba
77
import {MockSourceAdapter} from "../mocks/MockSourceAdapter.sol";
88

99
contract TestImmutableController is ImmutableController, MockSourceAdapter, BaseDestinationAdapter {
10-
constructor(uint8 decimals, uint256 _lockWindow, uint256 _maxTraversal, address[] memory _unlockers)
10+
constructor(
11+
uint8 decimals,
12+
uint256 _lockWindow,
13+
uint256 _maxTraversal,
14+
address[] memory _unlockers,
15+
uint256 _maxAge
16+
)
1117
MockSourceAdapter(decimals)
12-
ImmutableController(_lockWindow, _maxTraversal, _unlockers)
18+
ImmutableController(_lockWindow, _maxTraversal, _unlockers, _maxAge)
1319
BaseDestinationAdapter()
1420
{}
1521
}
@@ -19,6 +25,7 @@ contract ImmutableControllerTest is CommonTest {
1925
uint256 lockWindow = 60;
2026
uint256 maxTraversal = 10;
2127
address[] unlockers;
28+
uint256 maxAge = 86400;
2229

2330
uint256 lastUnlockTime = 1690000000;
2431

@@ -28,7 +35,7 @@ contract ImmutableControllerTest is CommonTest {
2835
unlockers.push(permissionedUnlocker);
2936

3037
vm.startPrank(owner);
31-
immutableController = new TestImmutableController(decimals, lockWindow, maxTraversal, unlockers);
38+
immutableController = new TestImmutableController(decimals, lockWindow, maxTraversal, unlockers, maxAge);
3239
vm.stopPrank();
3340
}
3441

0 commit comments

Comments
 (0)