This repository has been archived by the owner on Jun 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathOlympusPrice.v2.sol
743 lines (632 loc) · 31.5 KB
/
OlympusPrice.v2.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
/// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.15;
import "src/modules/PRICE/PRICE.v2.sol";
/// @title OlympusPriceV2
/// @author Oighty
/// @notice Provides current and historical prices for assets
contract OlympusPricev2 is PRICEv2 {
// DONE
// [X] Update functions for asset price feeds, strategies, etc.
// [X] Toggle MA on and off for an asset
// [X] Add "store" functions that call a view function, store the result, and return the value
// [X] Update add asset functions to account for new data structures
// [X] Update existing view functions to use new data structures
// [X] custom errors
// [X] implementation details in function comments
// [X] define and emit events: addAsset, removeAsset, update price feeds, update price strategy, update moving average
// ========== CONSTRUCTOR ========== //
/// @notice Constructor to create OlympusPrice V2
///
/// @param kernel_ Kernel address
/// @param decimals_ Decimals that all prices will be returned with
/// @param observationFrequency_ Frequency at which prices are stored for moving average
constructor(Kernel kernel_, uint8 decimals_, uint32 observationFrequency_) Module(kernel_) {
decimals = decimals_;
observationFrequency = observationFrequency_;
}
// ========== KERNEL FUNCTIONS ========== //
/// @inheritdoc Module
function KEYCODE() public pure override returns (Keycode) {
return toKeycode("PRICE");
}
/// @inheritdoc Module
function VERSION() external pure override returns (uint8 major, uint8 minor) {
major = 2;
minor = 0;
}
// ========== MODIFIERS ========== //
////////////////////////////////////////////////////////////////
// DATA FUNCTIONS //
////////////////////////////////////////////////////////////////
// ========== ASSET INFORMATION ========== //
/// @inheritdoc PRICEv2
function getAssets() external view override returns (address[] memory) {
return assets;
}
/// @inheritdoc PRICEv2
function getAssetData(address asset_) external view override returns (Asset memory) {
return _assetData[asset_];
}
// ========== ASSET PRICES ========== //
/// @inheritdoc PRICEv2
/// @dev Optimistically uses the cached price if it has been updated this block, otherwise calculates price dynamically
///
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - No price could be determined
function getPrice(address asset_) external view override returns (uint256) {
// Try to use the last price, must be updated on the current timestamp
// getPrice checks if asset is approved
(uint256 price, uint48 timestamp) = getPrice(asset_, Variant.LAST);
if (timestamp == uint48(block.timestamp)) return price;
// If last price is stale, use the current price
(price, ) = _getCurrentPrice(asset_);
return price;
}
/// @inheritdoc PRICEv2
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - No price could be determined
function getPrice(address asset_, uint48 maxAge_) external view override returns (uint256) {
// Try to use the last price, must be updated more recently than maxAge
// getPrice checks if asset is approved
(uint256 price, uint48 timestamp) = getPrice(asset_, Variant.LAST);
if (timestamp >= uint48(block.timestamp) - maxAge_) return price;
// If last price is stale, use the current price
(price, ) = _getCurrentPrice(asset_);
return price;
}
/// @inheritdoc PRICEv2
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - No price could be determined
/// @dev - An invalid variant is requested
function getPrice(
address asset_,
Variant variant_
) public view override returns (uint256 _price, uint48 _timestamp) {
// Check if asset is approved
if (!_assetData[asset_].approved) revert PRICE_AssetNotApproved(asset_);
// Route to correct price function based on requested variant
if (variant_ == Variant.CURRENT) {
return _getCurrentPrice(asset_);
} else if (variant_ == Variant.LAST) {
return _getLastPrice(asset_);
} else if (variant_ == Variant.MOVINGAVERAGE) {
return _getMovingAveragePrice(asset_);
}
}
/// @notice Gets the current price of the asset
/// @dev This function follows this logic:
/// @dev - Get the price from each feed
/// @dev - If using the moving average, append the moving average to the results
/// @dev - If there is only one price and it is not zero, return it
/// @dev - Process the prices with the configured strategy
///
/// @dev Will revert if:
/// @dev - The resulting price is zero
/// @dev - The configured strategy cannot aggregate the prices
///
/// @param asset_ Asset to get the price of
/// @return The price of the asset and the current block timestamp
function _getCurrentPrice(address asset_) internal view returns (uint256, uint48) {
Asset storage asset = _assetData[asset_];
// Iterate through feeds to get prices to aggregate with strategy
Component[] memory feeds = abi.decode(asset.feeds, (Component[]));
uint256 numFeeds = feeds.length;
uint256[] memory prices = asset.useMovingAverage
? new uint256[](numFeeds + 1)
: new uint256[](numFeeds);
uint8 _decimals = decimals; // cache in memory to save gas
for (uint256 i; i < numFeeds; ) {
(bool success_, bytes memory data_) = address(_getSubmoduleIfInstalled(feeds[i].target))
.staticcall(
abi.encodeWithSelector(feeds[i].selector, asset_, _decimals, feeds[i].params)
);
// Store price if successful, otherwise leave as zero
// Idea is that if you have several price calls and just
// one fails, it'll DOS the contract with this revert.
// We handle faulty feeds in the strategy contract.
if (success_) prices[i] = abi.decode(data_, (uint256));
unchecked {
++i;
}
}
// If moving average is used in strategy, add to end of prices array
if (asset.useMovingAverage) prices[numFeeds] = asset.cumulativeObs / asset.numObservations;
// If there is only one price, ensure it is not zero and return
// Otherwise, send to strategy to aggregate
if (prices.length == 1) {
if (prices[0] == 0) revert PRICE_PriceZero(asset_);
return (prices[0], uint48(block.timestamp));
} else {
// Get price from strategy
Component memory strategy = abi.decode(asset.strategy, (Component));
(bool success, bytes memory data) = address(_getSubmoduleIfInstalled(strategy.target))
.staticcall(abi.encodeWithSelector(strategy.selector, prices, strategy.params));
// Ensure call was successful
if (!success) revert PRICE_StrategyFailed(asset_, data);
// Decode asset price
uint256 price = abi.decode(data, (uint256));
// Ensure value is not zero
if (price == 0) revert PRICE_PriceZero(asset_);
return (price, uint48(block.timestamp));
}
}
/// @notice Gets the last cached price of the asset
/// @dev This function follows this logic:
/// @dev - Get the last observation stored for the asset and return it
///
/// @dev If no price has been ever cached, this function will automatically
/// @dev return (0, 0).
///
/// @param asset_ Asset to get the price of
/// @return The price of the asset and asset's last observation time
function _getLastPrice(address asset_) internal view returns (uint256, uint48) {
// Load asset data
Asset memory asset = _assetData[asset_];
// Get last observation stored for asset
uint256 lastPrice = asset.obs[
asset.nextObsIndex == 0 ? asset.numObservations - 1 : asset.nextObsIndex - 1
];
// Last price doesn't have to be checked for zero because it is checked before being stored
// Return last price and time
return (lastPrice, asset.lastObservationTime);
}
/// @notice Gets the moving average price of the asset
/// @dev This function follows this logic:
/// @dev - Calculate the moving average using the `cumulativeObs` / `numObservations`
///
/// @dev Will revert if:
/// @dev - The moving average is not stored for the asset
///
/// @param asset_ Asset to get the price of
/// @return The price of the asset and asset's last observation time
function _getMovingAveragePrice(address asset_) internal view returns (uint256, uint48) {
// Load asset data
Asset memory asset = _assetData[asset_];
// Check if moving average is stored for asset
if (!asset.storeMovingAverage) revert PRICE_MovingAverageNotStored(asset_);
// Calculate moving average
uint256 movingAverage = asset.cumulativeObs / asset.numObservations;
// Moving average doesn't have to be checked for zero because each value is checked before being stored
// Return moving average and time
return (movingAverage, asset.lastObservationTime);
}
/// @inheritdoc PRICEv2
/// @dev Optimistically uses the cached price if it has been updated this block, otherwise calculates price dynamically
function getPriceIn(address asset_, address base_) external view override returns (uint256) {
// Get the last price of each asset (getPrice checks if asset is approved)
(uint256 assetPrice, uint48 assetTime) = getPrice(asset_, Variant.LAST);
(uint256 basePrice, uint48 baseTime) = getPrice(base_, Variant.LAST);
// Try to use the last prices, timestamp must be current
// If stale, get current price
if (assetTime != uint48(block.timestamp)) {
(assetPrice, ) = _getCurrentPrice(asset_);
}
if (baseTime != uint48(block.timestamp)) {
(basePrice, ) = _getCurrentPrice(base_);
}
// Calculate the price of the asset in the base and return
return (assetPrice * 10 ** decimals) / basePrice;
}
/// @inheritdoc PRICEv2
function getPriceIn(
address asset_,
address base_,
uint48 maxAge_
) external view override returns (uint256) {
// Get the last price of each asset (getPrice checks if asset is approved)
(uint256 assetPrice, uint48 assetTime) = getPrice(asset_, Variant.LAST);
(uint256 basePrice, uint48 baseTime) = getPrice(base_, Variant.LAST);
// Try to use the last prices, timestamp must be no older than maxAge_
// If stale, get current price
if (assetTime < uint48(block.timestamp) - maxAge_) {
(assetPrice, ) = _getCurrentPrice(asset_);
}
if (baseTime < uint48(block.timestamp) - maxAge_) {
(basePrice, ) = _getCurrentPrice(base_);
}
// Calculate the price of the asset in the base and return
return (assetPrice * 10 ** decimals) / basePrice;
}
/// @inheritdoc PRICEv2
function getPriceIn(
address asset_,
address base_,
Variant variant_
) external view override returns (uint256, uint48) {
// Get the price of the asset (checks if approved)
(uint256 assetPrice, uint48 assetTime) = getPrice(asset_, variant_);
// Get the price of the base (checks if approved)
(uint256 basePrice, uint48 baseTime) = getPrice(base_, variant_);
// The updatedAt timestamp is the minimum of the two price updatedAt timestamps
uint48 updatedAt = assetTime < baseTime ? assetTime : baseTime;
// Calculate the price of the asset in the base
uint256 price = (assetPrice * 10 ** decimals) / basePrice;
return (price, updatedAt);
}
/// @inheritdoc PRICEv2
/// @dev Implements the following logic:
/// - Get the current price using `_getCurrentPrice()`
/// - Store the price in the asset's observation array at the index corresponding to the asset's value of `nextObsIndex`
/// - Updates the asset's `lastObservationTime` to the current block timestamp
/// - Increments the asset's `nextObsIndex` by 1, wrapping around to 0 if necessary
/// - If the asset is configured to store the moving average, update the `cumulativeObs` value subtracting the previous value and adding the new one
/// - Emit a `PriceStored` event
///
/// Will revert if:
/// - The asset is not approved
/// - The caller is not permissioned
/// - The price was not able to be determined
function storePrice(address asset_) public override permissioned {
Asset storage asset = _assetData[asset_];
// Check if asset is approved
if (!asset.approved) revert PRICE_AssetNotApproved(asset_);
// Get the current price for the asset
(uint256 price, uint48 currentTime) = _getCurrentPrice(asset_);
// Store the data in the obs index
uint256 oldestPrice = asset.obs[asset.nextObsIndex];
asset.obs[asset.nextObsIndex] = price;
// Update the last observation time and increment the next index
asset.lastObservationTime = currentTime;
asset.nextObsIndex = (asset.nextObsIndex + 1) % asset.numObservations;
// Update the cumulative observation, if storing the moving average
if (asset.storeMovingAverage)
asset.cumulativeObs = asset.cumulativeObs + price - oldestPrice;
// Emit event
emit PriceStored(asset_, price, currentTime);
}
// ========== ASSET MANAGEMENT ========== //
/// @inheritdoc PRICEv2
/// @dev Implements the following logic:
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the price strategy using `_updateAssetPriceStrategy()`
/// @dev - Sets the price feeds using `_updateAssetPriceFeeds()`
/// @dev - Sets the moving average data using `_updateAssetMovingAverage()`
/// @dev - Validates the configuration using `_getCurrentPrice()`, which will revert if there is a mis-configuration
/// @dev - Adds the asset to the `assets` array and marks it as approved
///
/// @dev Will revert if:
/// @dev - The caller is not permissioned
/// @dev - `asset_` is not a contract
/// @dev - `asset_` is already approved
/// @dev - The moving average is being used, but not stored
/// @dev - An empty strategy was specified, but the number of feeds requires a strategy
function addAsset(
address asset_,
bool storeMovingAverage_,
bool useMovingAverage_,
uint32 movingAverageDuration_,
uint48 lastObservationTime_,
uint256[] memory observations_,
Component memory strategy_,
Component[] memory feeds_
) external override permissioned {
// Check that asset is a contract
if (asset_.code.length == 0) revert PRICE_AssetNotContract(asset_);
Asset storage asset = _assetData[asset_];
// Ensure asset is not already added
if (asset.approved) revert PRICE_AssetAlreadyApproved(asset_);
// If not storing the moving average, validate that it's not being used by the strategy
if (useMovingAverage_ && !storeMovingAverage_)
revert PRICE_ParamsStoreMovingAverageRequired(asset_);
// Strategy cannot be zero if number of feeds + useMovingAverage is greater than 1
if (
(feeds_.length + (useMovingAverage_ ? 1 : 0)) > 1 &&
fromSubKeycode(strategy_.target) == bytes20(0)
)
revert PRICE_ParamsStrategyInsufficient(
asset_,
abi.encode(strategy_),
feeds_.length,
useMovingAverage_
);
// Update asset strategy data
_updateAssetPriceStrategy(asset_, strategy_, useMovingAverage_);
// Update asset price feed data
_updateAssetPriceFeeds(asset_, feeds_);
// Update asset moving average data
_updateAssetMovingAverage(
asset_,
storeMovingAverage_,
movingAverageDuration_,
lastObservationTime_,
observations_
);
// Validate configuration
_getCurrentPrice(asset_);
// Set asset as approved and add to array
asset.approved = true;
assets.push(asset_);
// Emit event
emit AssetAdded(asset_);
}
/// @inheritdoc PRICEv2
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - The caller is not permissioned
function removeAsset(address asset_) external override permissioned {
// Ensure asset is already added
if (!_assetData[asset_].approved) revert PRICE_AssetNotApproved(asset_);
// Remove asset from array
uint256 len = assets.length;
for (uint256 i; i < len; ) {
if (assets[i] == asset_) {
assets[i] = assets[len - 1];
assets.pop();
break;
}
unchecked {
++i;
}
}
// Remove asset from mapping
delete _assetData[asset_];
// Emit event
emit AssetRemoved(asset_);
}
/// @inheritdoc PRICEv2
/// @dev Implements the following logic:
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the price feeds using `_updateAssetPriceFeeds()`
/// @dev - Validates the configuration using `_getCurrentPrice()`, which will revert if there is a mis-configuration
///
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - The caller is not permissioned
/// @dev - `_updateAssetPriceFeeds()` reverts
/// @dev - `_getCurrentPrice()` reverts
function updateAssetPriceFeeds(
address asset_,
Component[] memory feeds_
) external override permissioned {
// Ensure asset is already added
if (!_assetData[asset_].approved) revert PRICE_AssetNotApproved(asset_);
_updateAssetPriceFeeds(asset_, feeds_);
// Validate the configuration
_getCurrentPrice(asset_);
// Emit event
emit AssetPriceFeedsUpdated(asset_);
}
/// @notice Updates the price feeds for the asset
/// @dev Implements the following logic:
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the price feeds for the asset
///
/// @dev Will revert if:
/// @dev - The number of feeds is zero
/// @dev - Any feed has a submodule that is not installed
///
/// @param asset_ Asset to update the price feeds for
/// @param feeds_ Array of price feed components
function _updateAssetPriceFeeds(address asset_, Component[] memory feeds_) internal {
// Validate feed component submodules are installed and update feed array
uint256 len = feeds_.length;
if (len == 0) revert PRICE_ParamsPriceFeedInsufficient(asset_, len, 1);
bytes32[] memory hashes = new bytes32[](len);
for (uint256 i; i < len; ) {
// Check that the submodule is installed
if (!_submoduleIsInstalled(feeds_[i].target))
revert PRICE_SubmoduleNotInstalled(asset_, abi.encode(feeds_[i].target));
// Confirm that the feed is not a duplicate by checking the hash against hashes of previous feeds in the array
bytes32 _hash = keccak256(
abi.encode(feeds_[i].target, feeds_[i].selector, feeds_[i].params)
);
for (uint256 j; j < i; ) {
if (_hash == hashes[j]) revert PRICE_DuplicatePriceFeed(asset_, i);
unchecked {
++j;
}
}
hashes[i] = _hash;
unchecked {
++i;
}
}
_assetData[asset_].feeds = abi.encode(feeds_);
}
/// @inheritdoc PRICEv2
/// @dev Implements the following logic:
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the price strategy using `_updateAssetPriceStrategy()`
/// @dev - Validates the configuration using `_getCurrentPrice()`, which will revert if there is a mis-configuration
///
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - The caller is not permissioned
/// @dev - The moving average is used, but is not stored
/// @dev - An empty strategy was specified, but the number of feeds requires a strategy
function updateAssetPriceStrategy(
address asset_,
Component memory strategy_,
bool useMovingAverage_
) external override permissioned {
// Ensure asset is already added
if (!_assetData[asset_].approved) revert PRICE_AssetNotApproved(asset_);
// Validate that the moving average is stored for the asset to use in strategy
if (useMovingAverage_ && !_assetData[asset_].storeMovingAverage)
revert PRICE_ParamsStoreMovingAverageRequired(asset_);
// Strategy cannot be zero if number of feeds + useMovingAverage is greater than 1
Component[] memory feeds = abi.decode(_assetData[asset_].feeds, (Component[]));
if (
(feeds.length + (useMovingAverage_ ? 1 : 0)) > 1 &&
fromSubKeycode(strategy_.target) == bytes20(0)
)
revert PRICE_ParamsStrategyInsufficient(
asset_,
abi.encode(strategy_),
feeds.length,
useMovingAverage_
);
_updateAssetPriceStrategy(asset_, strategy_, useMovingAverage_);
// Validate
_getCurrentPrice(asset_);
// Emit event
emit AssetPriceStrategyUpdated(asset_);
}
/// @notice Updates the price strategy for the asset
/// @dev Implements the following logic:
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the price strategy for the asset
/// @dev - Sets the `useMovingAverage` flag for the asset
///
/// @dev Will revert if:
/// @dev - The submodule used by the strategy is not installed
///
/// @param asset_ Asset to update the price strategy for
/// @param strategy_ Price strategy component
/// @param useMovingAverage_ Flag to indicate if the moving average should be used in the strategy
function _updateAssetPriceStrategy(
address asset_,
Component memory strategy_,
bool useMovingAverage_
) internal {
// Validate strategy component submodule is installed (if a strategy is being used)
// A strategy is optional if there is only one price feed being used.
// The number of feeds is checked in the external functions that call this one.
if (
fromSubKeycode(strategy_.target) != bytes20(0) &&
!_submoduleIsInstalled(strategy_.target)
) revert PRICE_SubmoduleNotInstalled(asset_, abi.encode(strategy_.target));
// Update the asset price strategy
_assetData[asset_].strategy = abi.encode(strategy_);
// Update whether the strategy uses a moving average (should be checked that the moving average is stored for the asset prior to sending to this function)
_assetData[asset_].useMovingAverage = useMovingAverage_;
}
/// @inheritdoc PRICEv2
/// @dev Implements the following logic:
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the moving average data using `_updateAssetMovingAverage()`
///
/// @dev Will revert if:
/// @dev - `asset_` is not approved
/// @dev - The caller is not permissioned
/// @dev - The moving average is used, but is not stored
///
/// @param asset_ Asset to update the moving average data for
/// @param storeMovingAverage_ Flag to indicate if the moving average should be stored
/// @param movingAverageDuration_ Duration of the moving average
/// @param lastObservationTime_ Timestamp of the last observation
/// @param observations_ Array of observations to store
function updateAssetMovingAverage(
address asset_,
bool storeMovingAverage_,
uint32 movingAverageDuration_,
uint48 lastObservationTime_,
uint256[] memory observations_
) external override permissioned {
// Ensure asset is already added
if (!_assetData[asset_].approved) revert PRICE_AssetNotApproved(asset_);
// If not storing the moving average, validate that it's not being used by the strategy.
// If it is, then you are moving from storing a moving average to not storing a moving average.
// First, change the strategy to not use the moving average, then update the moving average data.
if (_assetData[asset_].useMovingAverage && !storeMovingAverage_)
revert PRICE_ParamsStoreMovingAverageRequired(asset_);
_updateAssetMovingAverage(
asset_,
storeMovingAverage_,
movingAverageDuration_,
lastObservationTime_,
observations_
);
// Emit event
emit AssetMovingAverageUpdated(asset_);
}
/// @notice Updates the moving average data for the asset
/// @dev Implements the following logic:
/// @dev - Removes existing moving average data
/// @dev - Performs basic checks on the parameters
/// @dev - Sets the moving average data for the asset
/// @dev - If the moving average is not stored, gets the current price and stores it so that every asset has at least one cached value
///
/// @dev Will revert if:
/// @dev - `lastObservationTime_` is in the future
/// @dev - Any observation is zero
/// @dev - The number of observations provided is insufficient
///
/// @param asset_ Asset to update the moving average data for
/// @param storeMovingAverage_ Flag to indicate if the moving average should be stored
/// @param movingAverageDuration_ Duration of the moving average
/// @param lastObservationTime_ Timestamp of the last observation
/// @param observations_ Array of observations to store
function _updateAssetMovingAverage(
address asset_,
bool storeMovingAverage_,
uint32 movingAverageDuration_,
uint48 lastObservationTime_,
uint256[] memory observations_
) internal {
Asset storage asset = _assetData[asset_];
// Remove existing cached or moving average data, if any
if (asset.obs.length > 0) delete asset.obs;
// Ensure last observation time is not in the future
if (lastObservationTime_ > block.timestamp)
revert PRICE_ParamsLastObservationTimeInvalid(
asset_,
lastObservationTime_,
0,
uint48(block.timestamp)
);
if (storeMovingAverage_) {
// If storing a moving average, validate params
if (movingAverageDuration_ == 0 || movingAverageDuration_ % observationFrequency != 0)
revert PRICE_ParamsMovingAverageDurationInvalid(
asset_,
movingAverageDuration_,
observationFrequency
);
uint16 numObservations = uint16(movingAverageDuration_ / observationFrequency);
if (observations_.length != numObservations)
revert PRICE_ParamsInvalidObservationCount(
asset_,
observations_.length,
numObservations,
numObservations
);
asset.storeMovingAverage = true;
asset.movingAverageDuration = movingAverageDuration_;
asset.nextObsIndex = 0;
asset.numObservations = numObservations;
asset.lastObservationTime = lastObservationTime_;
asset.cumulativeObs = 0; // reset to zero before adding new observations
for (uint256 i; i < numObservations; ) {
if (observations_[i] == 0) revert PRICE_ParamsObservationZero(asset_, i);
asset.cumulativeObs += observations_[i];
asset.obs.push(observations_[i]);
unchecked {
++i;
}
}
// Emit Price Stored event for new cached value
emit PriceStored(asset_, observations_[numObservations - 1], lastObservationTime_);
} else {
// If not storing the moving average, validate that the array has at most one value (for caching)
if (observations_.length > 1)
revert PRICE_ParamsInvalidObservationCount(asset_, observations_.length, 0, 1);
asset.storeMovingAverage = false;
asset.movingAverageDuration = 0;
asset.nextObsIndex = 0;
asset.numObservations = 1;
if (observations_.length == 0) {
// If no observation provided, get the current price and store it
// We can do this here because we know the moving average isn't being stored
// and therefore, it is not being used in the strategy to calculate the price
(uint256 currentPrice, uint48 timestamp) = _getCurrentPrice(asset_);
asset.obs.push(currentPrice);
asset.lastObservationTime = timestamp;
// Emit Price Stored event for new cached value
emit PriceStored(asset_, currentPrice, timestamp);
} else {
// If an observation is provided, validate it and store it
if (observations_[0] == 0) revert PRICE_ParamsObservationZero(asset_, 0);
asset.obs.push(observations_[0]);
asset.lastObservationTime = lastObservationTime_;
// Emit Price Stored event for new cached value
emit PriceStored(asset_, observations_[0], lastObservationTime_);
}
// We don't track cumulativeObs when not storing the moving average, even though there is one data point in the array for caching
asset.cumulativeObs = 0;
}
}
}