@@ -5,6 +5,7 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I
55import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol " ;
66import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol " ;
77import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol " ;
8+ import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol " ;
89
910import { RLPReader } from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol " ;
1011
@@ -14,6 +15,7 @@ import { ReceiptProof } from "../hashi/prover/HashiProverStructs.sol";
1415import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol " ;
1516import { ICrossChainPaymaster } from "./interfaces/ICrossChainPaymaster.sol " ;
1617import { Math } from "@openzeppelin/contracts/utils/math/Math.sol " ;
18+ import { Arrays } from "@openzeppelin/contracts/utils/Arrays.sol " ;
1719import { SapphireTypes } from "./libraries/SapphireTypes.sol " ;
1820
1921/**
@@ -32,6 +34,8 @@ contract CrossChainPaymaster is
3234{
3335 using RLPReader for RLPReader.RLPItem;
3436 using RLPReader for bytes ;
37+ using EnumerableSet for EnumerableSet.AddressSet;
38+ using Arrays for uint256 [];
3539
3640 // ---------------------------------------------------------------------
3741 // Constants
@@ -42,6 +46,12 @@ contract CrossChainPaymaster is
4246 )
4347 );
4448
49+ /**
50+ * @notice Standard decimal scale for normalizing ROSE/USD feed prices
51+ * @dev All feed prices are normalized to this scale before averaging
52+ */
53+ uint8 internal constant NORMALIZED_DECIMALS = 18 ;
54+
4555 // ---------------------------------------------------------------------
4656 // Storage
4757 // ---------------------------------------------------------------------
@@ -53,9 +63,10 @@ contract CrossChainPaymaster is
5363 mapping (address => AggregatorV3Interface) public priceFeeds;
5464
5565 /**
56- * @notice ROSE/USD price feed (USD per 1 ROSE)
66+ * @notice Set of ROSE/USD price feeds (USD per 1 ROSE) from multiple sources
67+ * @dev Using EnumerableSet to prevent duplicates and allow efficient add/remove
5768 */
58- AggregatorV3Interface public roseUsdFeed ;
69+ EnumerableSet.AddressSet private _roseUsdFeeds ;
5970
6071 /**
6172 * @notice Token decimal mappings (token => decimals)
@@ -97,14 +108,14 @@ contract CrossChainPaymaster is
97108 * @param _shoyuBashi Hashi ShoyuBashi contract address
98109 * @param _limits Distribution limits for payments
99110 * @param _stalenessThreshold Maximum age for price data in seconds
100- * @param _roseUsdFeed Chainlink Aggregator for ROSE/USD
111+ * @param roseUsdFeeds Array of price feed addresses for ROSE/USD
101112 */
102113 function initialize (
103114 address _owner ,
104115 address _shoyuBashi ,
105116 SapphireTypes.DistributionLimits memory _limits ,
106117 uint256 _stalenessThreshold ,
107- address _roseUsdFeed
118+ address [] memory roseUsdFeeds
108119 ) external initializer {
109120 __ReentrancyGuard_init ();
110121 __HashiProverUpgradeable_init (_shoyuBashi);
@@ -114,8 +125,14 @@ contract CrossChainPaymaster is
114125 stalenessThreshold = _stalenessThreshold;
115126 limits = _limits;
116127
117- if (_roseUsdFeed == address (0 )) revert InvalidPriceFeed ();
118- roseUsdFeed = AggregatorV3Interface (_roseUsdFeed);
128+ // Require at least one ROSE/USD feed
129+ if (roseUsdFeeds.length == 0 ) revert NoRoseUsdFeeds ();
130+
131+ // Add all provided feeds to the set
132+ for (uint256 i = 0 ; i < roseUsdFeeds.length ; i++ ) {
133+ if (roseUsdFeeds[i] == address (0 )) revert InvalidPriceFeed ();
134+ if (! _roseUsdFeeds.add (roseUsdFeeds[i])) revert DuplicateRoseUsdFeed (roseUsdFeeds[i]);
135+ }
119136
120137 // Transfer ownership to requested owner if different
121138 if (_owner != owner ()) {
@@ -141,10 +158,19 @@ contract CrossChainPaymaster is
141158 /**
142159 * @inheritdoc ICrossChainPaymaster
143160 */
144- function setRoseUsdFeed (address feed ) external onlyOwner override {
161+ function addRoseUsdFeed (address feed ) external onlyOwner override {
145162 if (feed == address (0 )) revert InvalidPriceFeed ();
146- emit RoseUsdFeedUpdated (address (roseUsdFeed), feed);
147- roseUsdFeed = AggregatorV3Interface (feed);
163+ if (! _roseUsdFeeds.add (feed)) revert DuplicateRoseUsdFeed (feed);
164+ emit RoseUsdFeedAdded (feed);
165+ }
166+
167+ /**
168+ * @inheritdoc ICrossChainPaymaster
169+ */
170+ function removeRoseUsdFeed (address feed ) external onlyOwner override {
171+ if (_roseUsdFeeds.length () <= 1 ) revert NoRoseUsdFeeds ();
172+ if (! _roseUsdFeeds.remove (feed)) revert RoseUsdFeedNotFound (feed);
173+ emit RoseUsdFeedRemoved (feed);
148174 }
149175
150176 /**
@@ -304,6 +330,27 @@ contract CrossChainPaymaster is
304330 return processedPayments[paymentId];
305331 }
306332
333+ /**
334+ * @inheritdoc ICrossChainPaymaster
335+ */
336+ function getRoseUsdFeeds () external view override returns (address [] memory ) {
337+ return _roseUsdFeeds.values ();
338+ }
339+
340+ /**
341+ * @inheritdoc ICrossChainPaymaster
342+ */
343+ function getRoseUsdFeedCount () external view override returns (uint256 ) {
344+ return _roseUsdFeeds.length ();
345+ }
346+
347+ /**
348+ * @inheritdoc ICrossChainPaymaster
349+ */
350+ function getRoseUsdFeedAt (uint256 index ) external view override returns (address ) {
351+ return _roseUsdFeeds.at (index);
352+ }
353+
307354 // Decode PaymentInitiated log: [address, [topics...], data]
308355 /**
309356 * @notice Decodes a PaymentInitiated event log entry from RLP format
@@ -365,36 +412,133 @@ contract CrossChainPaymaster is
365412
366413 /**
367414 * @notice Converts token amount to ROSE amount using Chainlink-style price feeds
415+ * @dev Aggregates ROSE/USD price feeds as follows: uses the median value if
416+ * there are 3 or more valid feeds, the mean if there are 2 valid feeds, and
417+ * the direct value if there is only 1 valid feed. Skips invalid/stale feeds.
368418 * @param token The token address
369419 * @param tokenAmount The amount of tokens to convert
370420 * @return roseAmount The equivalent amount in ROSE
371421 */
372422 function _convertToRose (address token , uint256 tokenAmount ) internal view returns (uint256 roseAmount ) {
373423 AggregatorV3Interface tokenUsd = priceFeeds[token];
374424 if (address (tokenUsd) == address (0 )) revert NoPriceFeedForToken (token);
375- if (address (roseUsdFeed ) == address ( 0 )) revert InvalidPriceFeed ();
425+ if (_roseUsdFeeds. length ( ) == 0 ) revert NoRoseUsdFeeds ();
376426
427+ // Get token price data
377428 (uint80 tRound , int256 tPrice , , uint256 tUpdated , uint80 tAnsweredIn ) = tokenUsd.latestRoundData ();
378- (uint80 rRound , int256 rPrice , , uint256 rUpdated , uint80 rAnsweredIn ) = roseUsdFeed.latestRoundData ();
379429
380- // Validate prices & rounds
430+ // Validate token price
381431 if (tPrice <= 0 ) revert InvalidPrice (tPrice);
382- if (rPrice <= 0 ) revert InvalidPrice (rPrice);
383- if (tAnsweredIn < tRound || rAnsweredIn < rRound) revert StalePrice (0 , 0 );
432+ if (tAnsweredIn < tRound) revert StalePrice (0 , 0 );
384433 if (tUpdated == 0 || block .timestamp - tUpdated > stalenessThreshold) revert StalePrice (tUpdated, stalenessThreshold);
385- if (rUpdated == 0 || block .timestamp - rUpdated > stalenessThreshold) revert StalePrice (rUpdated, stalenessThreshold);
386434
387435 uint8 tokenDec = tokenDecimals[token];
388436 if (tokenDec == 0 ) tokenDec = 18 ;
389-
390437 uint8 tDec = tokenUsd.decimals ();
391- uint8 rDec = roseUsdFeed.decimals ();
438+
439+ // Collect valid ROSE/USD prices from all feeds, normalized to 18 decimals
440+ uint256 [] memory normalizedPrices = new uint256 [](_roseUsdFeeds.length ());
441+ uint256 validFeedCount = 0 ;
442+
443+ uint256 roseUsdFeedCount = _roseUsdFeeds.length ();
444+ for (uint256 i = 0 ; i < roseUsdFeedCount; i++ ) {
445+ address feedAddr = _roseUsdFeeds.at (i);
446+ AggregatorV3Interface roseFeed = AggregatorV3Interface (feedAddr);
447+
448+ try roseFeed.latestRoundData () returns (
449+ uint80 rRound ,
450+ int256 rPrice ,
451+ uint256 ,
452+ uint256 rUpdated ,
453+ uint80 rAnsweredIn
454+ ) {
455+ // Validate ROSE price from this feed
456+ if (rPrice <= 0 ) continue ; // Skip invalid price
457+ if (rAnsweredIn < rRound) continue ; // Skip stale round
458+ if (rUpdated == 0 || block .timestamp - rUpdated > stalenessThreshold) continue ; // Skip stale data
459+
460+ // Normalize price to 18 decimals before storing
461+ uint8 feedDecimals = roseFeed.decimals ();
462+ uint256 normalizedPrice;
463+
464+ if (feedDecimals < NORMALIZED_DECIMALS) {
465+ // Scale up: price * 10^(18 - feedDecimals)
466+ normalizedPrice = uint256 (rPrice) * (10 ** (NORMALIZED_DECIMALS - feedDecimals));
467+ } else if (feedDecimals > NORMALIZED_DECIMALS) {
468+ // Scale down: price / 10^(feedDecimals - 18)
469+ normalizedPrice = uint256 (rPrice) / (10 ** (feedDecimals - NORMALIZED_DECIMALS));
470+ } else {
471+ // Already 18 decimals
472+ normalizedPrice = uint256 (rPrice);
473+ }
474+
475+ normalizedPrices[validFeedCount] = normalizedPrice;
476+ validFeedCount++ ;
477+ } catch {
478+ // Skip feeds that revert
479+ continue ;
480+ }
481+ }
482+
483+ // Require at least one valid feed
484+ if (validFeedCount == 0 ) revert NoValidRoseUsdFeeds ();
485+
486+ // Calculate aggregated ROSE/USD price using appropriate method
487+ uint256 avgRosePrice;
488+ if (validFeedCount == 1 ) {
489+ // Single feed: use directly
490+ avgRosePrice = normalizedPrices[0 ];
491+ } else if (validFeedCount == 2 ) {
492+ // Two feeds: calculate mean (equivalent to median for 2 values)
493+ avgRosePrice = Math.average (normalizedPrices[0 ], normalizedPrices[1 ]);
494+ } else {
495+ // Three or more feeds: use median for outlier resistance
496+ avgRosePrice = _calculateMedian (normalizedPrices, validFeedCount);
497+ }
392498
393499 // roseAmount = tokenAmount * (tokenUsd / roseUsd) adjusted to 18 decimals
394- // = tokenAmount * tPrice * 10^rDec * 10^18 / (10^tokenDec * 10^tDec * rPrice )
500+ // = tokenAmount * tPrice * 10^18 * 10^18 / (10^tokenDec * 10^tDec * avgRosePrice )
395501 uint256 num = Math.mulDiv (tokenAmount, uint256 (tPrice), 10 ** tokenDec);
396- num = Math.mulDiv (num, 10 ** rDec, 10 ** tDec);
397- roseAmount = Math.mulDiv (num, 1e18 , uint256 (rPrice));
502+ num = Math.mulDiv (num, 10 ** NORMALIZED_DECIMALS, 10 ** tDec);
503+ roseAmount = Math.mulDiv (num, 1e18 , avgRosePrice);
504+ }
505+
506+ // ---------------------------------------------------------------------
507+ // Price Aggregation Helpers
508+ // ---------------------------------------------------------------------
509+
510+ /**
511+ * @notice Calculates the median of a price array
512+ * @dev Sorts the array using OpenZeppelin's Arrays.sort() and returns the middle value(s)
513+ * @param prices Array of normalized prices
514+ * @param count Number of valid prices in the array
515+ * @return The median price, if len(prices) is odd or mean of two median prices, if len(price) is even
516+
517+ */
518+ function _calculateMedian (uint256 [] memory prices , uint256 count ) internal pure returns (uint256 ) {
519+ require (count > 0 , "Empty price array " );
520+
521+ // If count < array length, we need to create a new array with only valid elements
522+ uint256 [] memory validPrices;
523+ if (count < prices.length ) {
524+ validPrices = new uint256 [](count);
525+ for (uint256 i = 0 ; i < count; i++ ) {
526+ validPrices[i] = prices[i];
527+ }
528+ } else {
529+ validPrices = prices;
530+ }
531+
532+ validPrices.sort ();
533+
534+ // Calculate median
535+ if (count % 2 == 1 ) {
536+ // Odd count: return middle element
537+ return validPrices[count / 2 ];
538+ } else {
539+ // Even count: return average of two middle elements
540+ return Math.average (validPrices[count / 2 - 1 ], validPrices[count / 2 ]);
541+ }
398542 }
399543
400544 /// @notice Accepts ROSE funding
0 commit comments