@ubk-labs/ubk-oracle 0.2.0 → 0.2.1

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.
@@ -39,4 +39,10 @@ library UBKOracleConstants {
39
39
  // Oracle Recursion
40
40
  // -----------------------------------------------------------------------
41
41
  uint256 public constant ORACLE_MAX_RECURSION_DEPTH = 5;
42
+
43
+ // -----------------------------------------------------------------------
44
+ // Chainlink Feed Decimals Bounds
45
+ // -----------------------------------------------------------------------
46
+ uint256 public constant ORACLE_MIN_CHAINLINK_FEED_DECIMALS = UBKConstants.GLOBAL_MIN_TOKEN_DECIMALS_ALLOWED; // Same as UBKDecimalsBounded (6)
47
+ uint256 public constant ORACLE_MAX_CHAINLINK_FEED_DECIMALS = UBKConstants.GLOBAL_MAX_TOKEN_DECIMALS_ALLOWED; // Same as UBKDecimalsBounded (18)
42
48
  }
@@ -88,7 +88,10 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
88
88
  mapping(address => bool) public isSupported;
89
89
 
90
90
  /// @notice Cache that tracks decimals for supported tokens.
91
- mapping(address => uint8) internal tokenDecimals;
91
+ mapping(address => uint8) internal tokenDecimalsCache;
92
+
93
+ /// @notice Cache that tracks decimals for registered Chainlink aggregators.
94
+ mapping(address => uint8) internal aggDecimalsCache;
92
95
 
93
96
  // -----------------------------------------------------------------------
94
97
  // Constructor & Modifiers
@@ -245,18 +248,44 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
245
248
  }
246
249
 
247
250
  /**
248
- * @notice Registers a Chainlink feed and validates its response.
249
- * @param token Asset token address.
250
- * @param feed Chainlink AggregatorV3 feed address.
251
- * @dev Ensures decimals 18 and feed returns a nonzero updatedAt value.
251
+ * @notice Registers a Chainlink price feed as the canonical oracle source for a token.
252
+ *
253
+ * @dev
254
+ * This function performs all required validation before binding a token to a
255
+ * Chainlink AggregatorV3 feed. If this call succeeds, the oracle guarantees that:
256
+ *
257
+ * - `token` is explicitly supported by the oracle and has validated, cached
258
+ * ERC-20 decimals within global bounds.
259
+ * - `feed` is a deployed AggregatorV3 contract with bounded, immutable decimals
260
+ * that are safe to cache and use for price normalization.
261
+ * - The feed is live and returning sane data at registration time
262
+ * (non-zero answer and timestamp).
263
+ *
264
+ * Upon successful registration:
265
+ * - The feed address and its decimals are cached immutably.
266
+ * - Manual pricing for `token` is disabled.
267
+ * - The token becomes eligible for pricing, conversion, and fallback logic
268
+ * across all oracle read paths.
269
+ *
270
+ * This function is administrative and MUST be called only during trusted
271
+ * configuration or governance actions. Runtime pricing paths assume that all
272
+ * invariants enforced here permanently hold.
273
+ *
274
+ * @param token The asset token whose price will be sourced from Chainlink.
275
+ * @param feed The Chainlink AggregatorV3 contract providing price data for `token`.
276
+ *
277
+ * @custom:invariant If this function returns, the oracle may safely resolve,
278
+ * normalize, and cache prices for `token` without performing
279
+ * any further structural validation on the feed.
252
280
  */
253
281
  function setChainlinkFeed(address token, address feed) external onlyOwner {
254
- _validateChainlinkFeed(token, feed);
255
-
256
- AggregatorV3Interface agg = AggregatorV3Interface(feed);
257
- uint8 decimals = agg.decimals();
258
- if (decimals > 18) revert InvalidFeedDecimals(feed, decimals);
282
+ // 1. Validate inputs and extract trusted feed metadata
283
+ (AggregatorV3Interface agg, uint8 aggDecimals) = _validateChainlinkFeed(
284
+ token,
285
+ feed
286
+ );
259
287
 
288
+ // 2. Ensure the feed is live and returning sane data
260
289
  try agg.latestRoundData() returns (
261
290
  uint80,
262
291
  int256 answer,
@@ -268,9 +297,13 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
268
297
  } catch {
269
298
  revert InvalidFeedContract(feed);
270
299
  }
271
- chainlinkFeeds[token] = feed;
272
- isManual[token] = false;
273
- _addSupportedToken(token);
300
+ // 3. Commit validated configuration to storage
301
+ chainlinkFeeds[token] = feed; // Map token to feed
302
+ aggDecimalsCache[feed] = aggDecimals; // Cache feed decimals
303
+ isManual[token] = false; // Set manual mode to false
304
+ _addSupportedToken(token); // Register support for token and cache token decimals.
305
+
306
+ // Emit event.
274
307
  emit ChainlinkFeedSet(token, feed);
275
308
  }
276
309
 
@@ -404,7 +437,7 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
404
437
  uint256 amount
405
438
  ) external view returns (uint256 usdValue) {
406
439
  if (amount == 0) return 0;
407
- uint8 decimals = tokenDecimals[token];
440
+ uint8 decimals = tokenDecimalsCache[token];
408
441
  uint256 normalized = (amount * UBKOracleConstants.WAD) /
409
442
  (10 ** decimals);
410
443
  uint256 price = _getPrice(token); // 18 decimals
@@ -433,7 +466,7 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
433
466
  uint256 usdAmount
434
467
  ) external view returns (uint256 tokenAmount) {
435
468
  if (usdAmount == 0) return 0;
436
- uint8 decimals = tokenDecimals[token];
469
+ uint8 decimals = tokenDecimalsCache[token];
437
470
  uint256 price = _getPrice(token); // 18 decimals
438
471
  uint256 normalized = (usdAmount * UBKOracleConstants.WAD) / price;
439
472
  tokenAmount = (normalized * (10 ** decimals)) / UBKOracleConstants.WAD;
@@ -503,8 +536,8 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
503
536
  address vault,
504
537
  address underlying
505
538
  ) internal checkRecursion returns (uint256 price) {
506
- uint8 shareDecimals = tokenDecimals[vault];
507
- uint8 underlyingDecimals = tokenDecimals[underlying];
539
+ uint8 shareDecimals = tokenDecimalsCache[vault];
540
+ uint8 underlyingDecimals = tokenDecimalsCache[underlying];
508
541
 
509
542
  uint256 oneShare = 10 ** shareDecimals;
510
543
  uint256 assetsPerShare = IERC4626(vault).convertToAssets(oneShare);
@@ -566,7 +599,7 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
566
599
  block.timestamp - updatedAt > stalePeriod[token]
567
600
  ) return (0, false);
568
601
 
569
- uint8 feedDecimals = AggregatorV3Interface(feed).decimals();
602
+ uint8 feedDecimals = aggDecimalsCache[feed];
570
603
  uint256 raw = uint256(answer);
571
604
 
572
605
  uint256 clPrice;
@@ -664,7 +697,7 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
664
697
  if (!isSupported[token]) {
665
698
  supportedTokens.push(token);
666
699
  isSupported[token] = true;
667
- tokenDecimals[token] = _validateTokenDecimals(token);
700
+ tokenDecimalsCache[token] = _validateTokenDecimals(token);
668
701
  _setStalePeriod(
669
702
  token,
670
703
  UBKOracleConstants.ORACLE_DEFAULT_STALE_PERIOD
@@ -674,18 +707,59 @@ contract UBKOracle is IUBKOracle, UBKDecimalsBounded, Ownable {
674
707
  }
675
708
 
676
709
  /**
677
- * @notice Validates assets and their associated Chainlink feeds before mutating system state.
678
- * @param token Asset token address.
679
- * @param feed Chainlink AggregatorV3 feed address.
680
- * @dev Ensures decimals 18 and feed returns a nonzero updatedAt value.
710
+ * @notice Validates and canonicalizes a Chainlink price feed configuration.
711
+ *
712
+ * @dev
713
+ * This function performs all one-time validation required before a Chainlink
714
+ * aggregator may be trusted by the oracle. If this function returns successfully,
715
+ * the following invariants are guaranteed for the lifetime of the configuration:
716
+ *
717
+ * - `token` is a non-zero ERC-20 address with validated decimals within
718
+ * the oracle’s global bounds.
719
+ * - `feed` is a non-zero deployed contract address implementing the
720
+ * Chainlink AggregatorV3 interface.
721
+ * - The aggregator’s reported decimals lie within the oracle’s accepted
722
+ * Chainlink decimal range and are safe to cache immutably.
723
+ *
724
+ * This function does NOT:
725
+ * - read or validate live pricing data,
726
+ * - mutate oracle state,
727
+ * - assume any particular price semantics beyond scale correctness.
728
+ *
729
+ * It exists to separate structural validation from runtime pricing logic,
730
+ * enabling deterministic gas usage and invariant-based reasoning throughout
731
+ * the oracle’s hot paths.
732
+ *
733
+ * @param token The asset token whose price will be sourced from the feed.
734
+ * @param feed The Chainlink AggregatorV3 contract providing price data for `token`.
735
+ *
736
+ * @return agg The validated AggregatorV3Interface instance.
737
+ * @return aggDecimals The validated and bounded number of decimals used by the aggregator.
738
+ *
739
+ * @custom:invariant If this function returns, `aggDecimals` may be safely cached
740
+ * and used for all future price normalization without further
741
+ * external calls.
681
742
  */
682
- function _validateChainlinkFeed(address token, address feed) internal view {
743
+ function _validateChainlinkFeed(
744
+ address token,
745
+ address feed
746
+ ) internal view returns (AggregatorV3Interface agg, uint8 aggDecimals) {
683
747
  if (token == address(0))
684
- revert ZeroAddress("UBKOracle::setChainlinkFeed", "token");
748
+ revert ZeroAddress("UBKOracle::_validateChainlinkFeed", "token");
685
749
  if (feed == address(0))
686
- revert ZeroAddress("UBKOracle::setChainlinkFeed", "feed");
750
+ revert ZeroAddress("UBKOracle::_validateChainlinkFeed", "feed");
687
751
  if (feed.code.length == 0) revert InvalidFeedContract(feed);
688
- _validateTokenDecimals(token);
752
+
753
+ _validateTokenDecimals(token); // Ensure ERC-20 tokens are in range.
754
+
755
+ agg = AggregatorV3Interface(feed);
756
+ aggDecimals = agg.decimals();
757
+
758
+ if (
759
+ aggDecimals <
760
+ UBKOracleConstants.ORACLE_MIN_CHAINLINK_FEED_DECIMALS ||
761
+ aggDecimals > UBKOracleConstants.ORACLE_MAX_CHAINLINK_FEED_DECIMALS
762
+ ) revert InvalidFeedDecimals(feed, aggDecimals); // Ensure aggregator decimals are in range.
689
763
  }
690
764
 
691
765
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubk-labs/ubk-oracle",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",