@ubk-labs/ubk-oracle 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,6 @@ pragma solidity ^0.8.20;
3
3
 
4
4
  import "@ubk-labs/ubk-commons/contracts/constants/UBKConstants.sol";
5
5
 
6
-
7
6
  library UBKOracleConstants {
8
7
  // -----------------------------------------------------------------------
9
8
  // UBK System Constants
@@ -27,12 +26,14 @@ library UBKOracleConstants {
27
26
  // -----------------------------------------------------------------------
28
27
  // Oracle Staleness Periods
29
28
  // -----------------------------------------------------------------------
30
- uint256 public constant ORACLE_DEFAULT_STALE_PERIOD = 1 hours;
31
29
  uint256 public constant ORACLE_MIN_STALE_PERIOD = 1 hours;
32
- uint256 public constant ORACLE_MAX_STALE_PERIOD = 3 hours;
30
+ uint256 public constant ORACLE_MAX_STALE_PERIOD = 48 hours;
31
+
32
+ uint256 public constant ORACLE_DEFAULT_STALE_FALLBACK_MULTIPLIER = 2; //2x stale
33
+ uint256 public constant ORACLE_MAX_STALE_FALLBACK_MULTIPLIER = 3; //3x stale
33
34
 
34
35
  // -----------------------------------------------------------------------
35
36
  // Oracle Recursion
36
37
  // -----------------------------------------------------------------------
37
- uint256 public constant MAX_RECURSION_DEPTH = 5;
38
+ uint256 public constant ORACLE_MAX_RECURSION_DEPTH = 5;
38
39
  }
@@ -14,7 +14,7 @@ import "../constants/UBKOracleConstants.sol";
14
14
 
15
15
  /**
16
16
  * @title Oracle
17
- * @notice This contract is an implementation of the IOracle interface.
17
+ * @notice This contract is an implementation of the IUBKOracle interface.
18
18
  *
19
19
  * @dev
20
20
  * The oracle computes normalized 1e18 prices for all supported assets,
@@ -63,11 +63,10 @@ contract UBKOracle is IUBKOracle, Ownable {
63
63
  mapping(address => VaultRateBounds) public vaultRateBounds;
64
64
 
65
65
  /// @notice Maximum staleness period for Chainlink feeds (seconds).
66
- uint256 public stalePeriod = UBKOracleConstants.ORACLE_DEFAULT_STALE_PERIOD;
66
+ mapping(address => uint256) public stalePeriod;
67
67
 
68
68
  /// @notice Staleness tolerance for fallback prices (seconds).
69
- uint256 public fallbackStalePeriod =
70
- UBKOracleConstants.ORACLE_DEFAULT_STALE_PERIOD * 2;
69
+ mapping(address => uint256) public fallbackStalePeriod;
71
70
 
72
71
  OracleMode public mode = OracleMode.NORMAL;
73
72
 
@@ -102,7 +101,7 @@ contract UBKOracle is IUBKOracle, Ownable {
102
101
 
103
102
  /// @notice Prevents infinite recursion when resolving nested ERC4626 vaults.
104
103
  modifier checkRecursion() {
105
- if (_recursionDepth >= UBKOracleConstants.MAX_RECURSION_DEPTH)
104
+ if (_recursionDepth >= UBKOracleConstants.ORACLE_MAX_RECURSION_DEPTH)
106
105
  revert RecursiveResolution(address(0));
107
106
  _recursionDepth++;
108
107
  _;
@@ -125,16 +124,20 @@ contract UBKOracle is IUBKOracle, Ownable {
125
124
 
126
125
  /**
127
126
  * @notice Sets the maximum time (in seconds) a Chainlink feed value is valid.
128
- * @param period The new staleness threshold.
127
+ * @param period The new fallback staleness threshold. Must be 1.5x of stalePeriod of the same token.
129
128
  * @dev Must lie within [UBKOracleConstants.ORACLE_MIN_STALE_PERIOD, UBKOracleConstants.ORACLE_MAX_STALE_PERIOD].
130
129
  */
131
- function setStalePeriod(uint256 period) external onlyOwner {
130
+ function setStalePeriod(address token, uint256 period) external onlyOwner {
132
131
  if (
133
132
  period < UBKOracleConstants.ORACLE_MIN_STALE_PERIOD ||
134
133
  period > UBKOracleConstants.ORACLE_MAX_STALE_PERIOD
135
134
  ) revert InvalidStalePeriod(period);
136
- stalePeriod = period;
137
- emit StalePeriodUpdated(period);
135
+ stalePeriod[token] = period;
136
+ fallbackStalePeriod[token] =
137
+ UBKOracleConstants.ORACLE_DEFAULT_STALE_FALLBACK_MULTIPLIER *
138
+ period; //Minimum fallback period should be 2x stalePeriod[token].
139
+ emit StalePeriodUpdated(token, stalePeriod[token]);
140
+ emit FallbackStalePeriodUpdated(token, fallbackStalePeriod[token]);
138
141
  }
139
142
 
140
143
  /**
@@ -142,10 +145,17 @@ contract UBKOracle is IUBKOracle, Ownable {
142
145
  * @param period Maximum allowed seconds for fallback validity.
143
146
  * @dev Must be ≥ stalePeriod to remain meaningful.
144
147
  */
145
- function setFallbackStalePeriod(uint256 period) external onlyOwner {
146
- if (period < stalePeriod) revert InvalidStalePeriod(period);
147
- fallbackStalePeriod = period;
148
- emit FallbackStalePeriodUpdated(period);
148
+ function setFallbackStalePeriod(
149
+ address token,
150
+ uint256 period
151
+ ) external onlyOwner {
152
+ uint256 stalePeriodToken = stalePeriod[token]; // Default stale period of token.
153
+ uint256 maxFallbackPeriod = stalePeriodToken *
154
+ UBKOracleConstants.ORACLE_MAX_STALE_FALLBACK_MULTIPLIER; // The absolute maximum fallback stale period allowed. (3x)
155
+ if (period < stalePeriodToken || period > maxFallbackPeriod)
156
+ revert InvalidStalePeriod(period);
157
+ fallbackStalePeriod[token] = period;
158
+ emit FallbackStalePeriodUpdated(token, period);
149
159
  }
150
160
 
151
161
  /**
@@ -188,12 +198,16 @@ contract UBKOracle is IUBKOracle, Ownable {
188
198
  ) revert InvalidManualPrice(token, price);
189
199
 
190
200
  LastValidPrice memory lv = lastValidPrice[token];
191
- if (lv.price > 0 && block.timestamp - lv.timestamp <= stalePeriod) {
201
+ if (
202
+ lv.price > 0 && block.timestamp - lv.timestamp <= stalePeriod[token]
203
+ ) {
192
204
  uint256 lowerBound = (lv.price *
193
- (UBKOracleConstants.WAD - UBKOracleConstants.ORACLE_MANUAL_PRICE_MAX_DELTA_WAD)) /
205
+ (UBKOracleConstants.WAD -
206
+ UBKOracleConstants.ORACLE_MANUAL_PRICE_MAX_DELTA_WAD)) /
194
207
  UBKOracleConstants.WAD;
195
208
  uint256 upperBound = (lv.price *
196
- (UBKOracleConstants.WAD + UBKOracleConstants.ORACLE_MANUAL_PRICE_MAX_DELTA_WAD)) /
209
+ (UBKOracleConstants.WAD +
210
+ UBKOracleConstants.ORACLE_MANUAL_PRICE_MAX_DELTA_WAD)) /
197
211
  UBKOracleConstants.WAD;
198
212
  if (price < lowerBound || price > upperBound)
199
213
  revert InvalidManualPrice(token, price);
@@ -245,7 +259,6 @@ contract UBKOracle is IUBKOracle, Ownable {
245
259
  } catch {
246
260
  revert InvalidFeedContract(feed);
247
261
  }
248
-
249
262
  chainlinkFeeds[token] = feed;
250
263
  isManual[token] = false;
251
264
  _addSupportedToken(token);
@@ -299,6 +312,36 @@ contract UBKOracle is IUBKOracle, Ownable {
299
312
  return _fetchAndUpdatePrice(token);
300
313
  }
301
314
 
315
+ /**
316
+ * @notice Batch price fetch & update for multiple tokens.
317
+ * @param tokens Array of asset token addresses.
318
+ * @return prices Array of fresh prices in 1e18 precision.
319
+ *
320
+ * @dev
321
+ * - Runs whenNotPaused modifier.
322
+ * - Reverts if any token is zero address.
323
+ * - Each iteration calls the same internal `_fetchAndUpdatePrice`.
324
+ * - Gas-efficient: msg.sender checked once, calldata parsed once.
325
+ */
326
+ function fetchAndUpdatePrice(
327
+ address[] calldata tokens
328
+ ) external whenNotPaused returns (uint256[] memory prices) {
329
+ uint256 len = tokens.length;
330
+ prices = new uint256[](len);
331
+
332
+ for (uint256 i = 0; i < len; ++i) {
333
+ address token = tokens[i];
334
+ if (token == address(0)) {
335
+ revert ZeroAddress(
336
+ "UBKOracle::fetchAndUpdatePriceBatch",
337
+ "token"
338
+ );
339
+ }
340
+
341
+ prices[i] = _fetchAndUpdatePrice(token);
342
+ }
343
+ }
344
+
302
345
  /**
303
346
  * @notice Returns age of last cached price in seconds.
304
347
  * @param token Token address.
@@ -310,15 +353,12 @@ contract UBKOracle is IUBKOracle, Ownable {
310
353
  return block.timestamp - lv.timestamp;
311
354
  }
312
355
 
313
- /**
356
+ /** Public wrapper for _isPriceFresh
314
357
  * @notice Checks if cached price is within freshness threshold.
315
- * @param token Token address.
316
358
  * @return isFresh True if price updated ≤ stalePeriod ago.
317
359
  */
318
360
  function isPriceFresh(address token) external view returns (bool isFresh) {
319
- LastValidPrice memory lv = lastValidPrice[token];
320
- return (lv.timestamp != 0 &&
321
- block.timestamp - lv.timestamp <= stalePeriod);
361
+ return _isPriceFresh(token);
322
362
  }
323
363
 
324
364
  /**
@@ -352,7 +392,8 @@ contract UBKOracle is IUBKOracle, Ownable {
352
392
  ) external view returns (uint256 usdValue) {
353
393
  if (amount == 0) return 0;
354
394
  uint8 decimals = IERC20Metadata(token).decimals();
355
- uint256 normalized = (amount * UBKOracleConstants.WAD) / (10 ** decimals);
395
+ uint256 normalized = (amount * UBKOracleConstants.WAD) /
396
+ (10 ** decimals);
356
397
  uint256 price = _getPrice(token); // 18 decimals
357
398
  usdValue = (normalized * price) / UBKOracleConstants.WAD;
358
399
  }
@@ -385,13 +426,26 @@ contract UBKOracle is IUBKOracle, Ownable {
385
426
  * @return price Cached price in 1e18 precision.
386
427
  */
387
428
  function _getPrice(address token) internal view returns (uint256) {
388
- if (token == address(0)) revert ZeroAddress("UBKOracle::getPrice", "token");
429
+ if (token == address(0))
430
+ revert ZeroAddress("UBKOracle::getPrice", "token");
389
431
  LastValidPrice memory lv = lastValidPrice[token];
390
432
  if (lv.price == 0) revert NoFallbackPrice(token);
391
- if (!this.isPriceFresh(token))
433
+ if (!_isPriceFresh(token))
392
434
  revert StalePrice(token, lv.timestamp, block.timestamp);
393
435
  return lv.price;
394
436
  }
437
+
438
+ /**
439
+ * @notice Checks if cached price is within freshness threshold.
440
+ * @param token Token address.
441
+ * @return isFresh True if price updated ≤ stalePeriod ago.
442
+ */
443
+ function _isPriceFresh(address token) internal view returns (bool isFresh) {
444
+ LastValidPrice memory lv = lastValidPrice[token];
445
+ return (lv.timestamp != 0 &&
446
+ block.timestamp - lv.timestamp <= stalePeriod[token]);
447
+ }
448
+
395
449
  /**
396
450
  * @notice Resolves the fair price of an ERC4626 vault share.
397
451
  * @param vault ERC4626 vault token address.
@@ -428,13 +482,13 @@ contract UBKOracle is IUBKOracle, Ownable {
428
482
  if (rate > maxRate || rate < minRate)
429
483
  revert SuspiciousVaultRate(vault, rate);
430
484
 
431
- uint256 underlyingPrice = resolvePrice(underlying);
485
+ uint256 underlyingPrice = _resolvePrice(underlying);
432
486
  return (underlyingPrice * rate) / UBKOracleConstants.WAD;
433
487
  }
434
488
 
435
489
  /**
436
490
  * @notice Fetches and validates the latest Chainlink feed price.
437
- * @param feed The address of the Chainlink AggregatorV3 feed.
491
+ * @param token The address of the token whose feed needs to be read.
438
492
  * @return price The normalized price (1e18 precision).
439
493
  * @return valid Boolean indicating whether the feed result is valid.
440
494
  *
@@ -448,8 +502,9 @@ contract UBKOracle is IUBKOracle, Ownable {
448
502
  * If any condition fails or the feed call reverts, returns `(0, false)`.
449
503
  */
450
504
  function _getChainlinkPrice(
451
- address feed
505
+ address token
452
506
  ) internal view returns (uint256 price, bool valid) {
507
+ address feed = chainlinkFeeds[token];
453
508
  try AggregatorV3Interface(feed).latestRoundData() returns (
454
509
  uint80,
455
510
  int256 answer,
@@ -460,7 +515,7 @@ contract UBKOracle is IUBKOracle, Ownable {
460
515
  if (
461
516
  answer <= 0 ||
462
517
  updatedAt == 0 ||
463
- block.timestamp - updatedAt > stalePeriod
518
+ block.timestamp - updatedAt > stalePeriod[token]
464
519
  ) return (0, false);
465
520
 
466
521
  uint8 feedDecimals = AggregatorV3Interface(feed).decimals();
@@ -494,7 +549,7 @@ contract UBKOracle is IUBKOracle, Ownable {
494
549
  * @return price Resolved fair price in 1e18 precision.
495
550
  * @dev May trigger state updates if underlying vaults are resolved.
496
551
  */
497
- function resolvePrice(address token) public returns (uint256 price) {
552
+ function _resolvePrice(address token) internal returns (uint256 price) {
498
553
  if (isManual[token]) return manualPrices[token];
499
554
 
500
555
  address underlying = erc4626Underlying[token];
@@ -503,12 +558,12 @@ contract UBKOracle is IUBKOracle, Ownable {
503
558
  address feed = chainlinkFeeds[token];
504
559
  if (feed == address(0)) revert NoPriceFeed(token);
505
560
 
506
- (uint256 clPrice, bool valid) = _getChainlinkPrice(feed);
561
+ (uint256 clPrice, bool valid) = _getChainlinkPrice(token);
507
562
  if (valid) return clPrice;
508
563
 
509
564
  LastValidPrice memory lv = lastValidPrice[token];
510
565
  if (lv.price == 0) revert NoFallbackPrice(token);
511
- if (block.timestamp - lv.timestamp > fallbackStalePeriod)
566
+ if (block.timestamp - lv.timestamp > fallbackStalePeriod[token])
512
567
  revert StaleFallback(token);
513
568
 
514
569
  emit OracleFallbackUsed(
@@ -529,7 +584,7 @@ contract UBKOracle is IUBKOracle, Ownable {
529
584
  function _fetchAndUpdatePrice(
530
585
  address token
531
586
  ) internal returns (uint256 price) {
532
- price = resolvePrice(token);
587
+ price = _resolvePrice(token);
533
588
  if (
534
589
  price < UBKOracleConstants.ORACLE_MIN_ABSOLUTE_PRICE_WAD ||
535
590
  price > UBKOracleConstants.ORACLE_MAX_ABSOLUTE_PRICE_WAD
@@ -2,6 +2,10 @@
2
2
  pragma solidity ^0.8.20;
3
3
 
4
4
  interface IUBKOracle {
5
+ // -----------------------------------------------------------------------
6
+ // ENUMS & STRUCTS
7
+ // -----------------------------------------------------------------------
8
+
5
9
  /// @notice Operational state of the oracle.
6
10
  enum OracleMode {
7
11
  NORMAL,
@@ -14,31 +18,40 @@ interface IUBKOracle {
14
18
  uint256 timestamp;
15
19
  }
16
20
 
17
- /// @notice Custom vault rate bounds (min/max multiplier per vault).
21
+ /// @notice Vault-specific allowable min/max exchange rates.
18
22
  struct VaultRateBounds {
19
23
  uint256 minRate;
20
24
  uint256 maxRate;
21
25
  }
22
26
 
23
- /// @notice Oracle specific events for subgraph indexing.
27
+ // -----------------------------------------------------------------------
28
+ // EVENTS (for Subgraph indexing)
29
+ // -----------------------------------------------------------------------
30
+
24
31
  event ChainlinkFeedSet(address indexed token, address indexed feed);
25
32
  event ERC4626Registered(address indexed vault, address indexed underlying);
26
33
  event TokenSupportAdded(address indexed token);
34
+
27
35
  event ManualPriceSet(address indexed token, uint256 price);
28
36
  event ManualModeEnabled(address indexed token, bool enabled);
29
- event StalePeriodUpdated(uint256 newPeriod);
37
+
38
+ event StalePeriodUpdated(address indexed token, uint256 newPeriod);
39
+ event FallbackStalePeriodUpdated(address indexed token, uint256 newPeriod);
40
+
30
41
  event OracleModeChanged(OracleMode oldMode, OracleMode newMode);
31
- event FallbackStalePeriodUpdated(uint256 newPeriod);
42
+
32
43
  event VaultRateBoundsSet(
33
44
  address indexed vault,
34
45
  uint256 minRate,
35
46
  uint256 maxRate
36
47
  );
48
+
37
49
  event LastValidPriceUpdated(
38
50
  address indexed token,
39
51
  uint256 price,
40
52
  uint256 timestamp
41
53
  );
54
+
42
55
  event OracleFallbackUsed(
43
56
  address indexed token,
44
57
  uint256 lastValid,
@@ -46,17 +59,90 @@ interface IUBKOracle {
46
59
  string reason
47
60
  );
48
61
 
49
- // View Functions
62
+ // -----------------------------------------------------------------------
63
+ // VIEW PRICING API (Consumer-facing)
64
+ // -----------------------------------------------------------------------
65
+
66
+ /**
67
+ * @notice Returns the cached fair price for a token (1e18 precision).
68
+ * @dev Reverts if the price is stale or no valid price exists.
69
+ */
50
70
  function getPrice(address token) external view returns (uint256);
71
+
72
+ /**
73
+ * @notice Converts a token amount (native decimals) into USD value (1e18).
74
+ */
51
75
  function toUSD(
52
76
  address token,
53
77
  uint256 amount
54
78
  ) external view returns (uint256 usdValue);
79
+
80
+ /**
81
+ * @notice Converts a USD amount (1e18) into token units (native decimals).
82
+ */
55
83
  function fromUSD(
56
84
  address token,
57
85
  uint256 usdAmount
58
86
  ) external view returns (uint256 tokenAmount);
59
87
 
60
- // Mutators
88
+ // -----------------------------------------------------------------------
89
+ // KEEPER / MUTATOR FUNCTIONS
90
+ // -----------------------------------------------------------------------
91
+
92
+ /**
93
+ * @notice Fetches and resolves the token price, then persists it as lastValidPrice.
94
+ * @dev Keeper entrypoint. May revert if price resolution fails.
95
+ */
61
96
  function fetchAndUpdatePrice(address token) external returns (uint256);
97
+ function fetchAndUpdatePrice(
98
+ address[] calldata tokens
99
+ ) external returns (uint256[] memory);
100
+ // -----------------------------------------------------------------------
101
+ // ADMIN / GOVERNANCE CONFIGURATION
102
+ // -----------------------------------------------------------------------
103
+
104
+ /**
105
+ * @notice Sets the operating mode of the oracle (NORMAL or PAUSED).
106
+ * @dev PAUSED mode disables fetchAndUpdatePrice().
107
+ */
108
+ function setOracleMode(OracleMode newMode) external;
109
+
110
+ /**
111
+ * @notice Sets the maximum allowed staleness for Chainlink feed data.
112
+ */
113
+ function setStalePeriod(address token, uint256 period) external;
114
+
115
+ /**
116
+ * @notice Sets the fallback staleness threshold for relying on lastValidPrice.
117
+ */
118
+ function setFallbackStalePeriod(address token, uint256 period) external;
119
+
120
+ /**
121
+ * @notice Defines allowable min/max ERC4626 exchange rate bounds for a vault.
122
+ */
123
+ function setVaultRateBounds(
124
+ address vault,
125
+ uint256 minRate,
126
+ uint256 maxRate
127
+ ) external;
128
+
129
+ /**
130
+ * @notice Manually sets a token's price (1e18), constrained by ±10% of lastValidPrice.
131
+ */
132
+ function setManualPrice(address token, uint256 price) external;
133
+
134
+ /**
135
+ * @notice Disables manual pricing mode for a token.
136
+ */
137
+ function disableManualPrice(address token) external;
138
+
139
+ /**
140
+ * @notice Registers or updates a Chainlink feed for a token.
141
+ */
142
+ function setChainlinkFeed(address token, address feed) external;
143
+
144
+ /**
145
+ * @notice Registers an ERC4626 vault and its underlying token for valuation.
146
+ */
147
+ function setERC4626Vault(address vault, address underlying) external;
62
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubk-labs/ubk-oracle",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Oracle supporting ERC-20 and ERC-4626 assets for use in decentralized financial applications.",
5
5
  "scripts": {
6
6
  "build": "npx hardhat compile",