diff --git a/contracts/src/v0.8/automation/dev/MercuryRegistry.sol b/contracts/src/v0.8/automation/dev/MercuryRegistry.sol new file mode 100644 index 00000000000..e47b33fc012 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/MercuryRegistry.sol @@ -0,0 +1,312 @@ +pragma solidity 0.8.6; + +import "../../shared/access/ConfirmedOwner.sol"; +import "../interfaces/AutomationCompatibleInterface.sol"; +import "../interfaces/StreamsLookupCompatibleInterface.sol"; +import "../../ChainSpecificUtil.sol"; + +/*--------------------------------------------------------------------------------------------------------------------+ +| Mercury + Automation | +| ________________ | +| This implementation allows for an on-chain registry of price feed data to be maintained and updated by Automation | +| nodes. The upkeep provides the following advantages: | +| - Node operator savings. The single committee of automation nodes is able to update all price feed data using | +| off-chain feed data. | +| - Fetch batches of price data. All price feed data is held on the same contract, so a contract that needs | +| multiple sets of feed data can fetch them while paying for only one external call. | +| - Scalability. Feeds can be added or removed from the contract with a single contract call, and the number of | +| feeds that the registry can store is unbounded. | +| | +| Key Contracts: | +| - `MercuryRegistry.sol` - stores price feed data and implements core logic. | +| - `MercuryRegistryBatchUpkeep.sol` - enables batching for the registry. | +| - `MercuryRegistry.t.sol` - contains foundry tests to demonstrate various flows. | +| | +| NOTE: This contract uses Mercury v0.2. Automation will likely upgrade to v0.3 eventually, which may change some | +| components such as the Report struct, verification, and the StreamsLookup revert. | +| | +| TODO: | +| - Optimize gas consumption. | +-+---------------------------------------------------------------------------------------------------------------------*/ +contract MercuryRegistry is ConfirmedOwner, AutomationCompatibleInterface, StreamsLookupCompatibleInterface { + error DuplicateFeed(string feedId); + error FeedNotActive(string feedId); + error StaleReport(string feedId, uint32 currentTimestamp, uint32 incomingTimestamp); + error InvalidFeeds(); + + // Feed object used for storing feed data. + // not included but contained in reports: + // - blocknumberUpperBound + // - upperBlockhash + // - blocknumberLowerBound + // - currentBlockTimestamp + struct Feed { + uint32 observationsTimestamp; // the timestamp of the most recent data assigned to this feed + int192 price; // the current price of the feed + int192 bid; // the current bid price of the feed + int192 ask; // the current ask price of the feed + string feedName; // the name of the feed + string feedId; // the id of the feed (hex encoded) + bool active; // true if the feed is being actively updated, otherwise false + int192 deviationPercentagePPM; // acceptable deviation threshold - 1.5% = 15_000, 100% = 1_000_000, etc.. + uint32 stalenessSeconds; // acceptable staleness threshold - 60 = 1 minute, 300 = 5 minutes, etc.. + } + + // Report object obtained from off-chain Mercury server. + struct Report { + bytes32 feedId; // the feed Id of the report + uint32 observationsTimestamp; // the timestamp of when the data was observed + int192 price; // the median value of the OCR round + int192 bid; // the median bid of the OCR round + int192 ask; // the median ask if the OCR round + uint64 blocknumberUpperBound; // the highest block observed at the time the report was generated + bytes32 upperBlockhash; // the blockhash of the highest block observed + uint64 blocknumberLowerBound; // the lowest block observed at the time the report was generated + uint64 currentBlockTimestamp; // the timestamp of the highest block observed + } + + event FeedUpdated(uint32 observationsTimestamp, int192 price, int192 bid, int192 ask, string feedId); + + uint32 private constant MIN_GAS_FOR_PERFORM = 200_000; + + string constant c_feedParamKey = "feedIdHex"; // for Mercury v0.2 - format by which feeds are identified + string constant c_timeParamKey = "blockNumber"; // for Mercury v0.2 - format by which feeds are filtered to be sufficiently recent + IVerifierProxy public s_verifier; // for Mercury v0.2 - verifies off-chain reports + + int192 constant scale = 1_000_000; // a scalar used for measuring deviation with precision + + string[] public s_feeds; // list of feed Ids + mapping(string => Feed) public s_feedMapping; // mapping of feed Ids to stored feed data + + constructor( + string[] memory feedIds, + string[] memory feedNames, + int192[] memory deviationPercentagePPMs, + uint32[] memory stalenessSeconds, + address verifier + ) ConfirmedOwner(msg.sender) { + s_verifier = IVerifierProxy(verifier); + + // Store desired feeds. + setFeeds(feedIds, feedNames, deviationPercentagePPMs, stalenessSeconds); + } + + // Returns a user-defined batch of feed data, based on the on-chain state. + function getLatestFeedData(string[] memory feedIds) external view returns (Feed[] memory) { + Feed[] memory feeds = new Feed[](feedIds.length); + for (uint256 i = 0; i < feedIds.length; i++) { + feeds[i] = s_feedMapping[feedIds[i]]; + } + + return feeds; + } + + // Invoke a feed lookup through the checkUpkeep function. Expected to run on a cron schedule. + function checkUpkeep(bytes calldata /* data */) external view override returns (bool, bytes memory) { + string[] memory feeds = s_feeds; + return revertForFeedLookup(feeds); + } + + // Extracted from `checkUpkeep` for batching purposes. + function revertForFeedLookup(string[] memory feeds) public view returns (bool, bytes memory) { + uint256 blockNumber = ChainSpecificUtil.getBlockNumber(); + revert StreamsLookup(c_feedParamKey, feeds, c_timeParamKey, blockNumber, ""); + } + + // Filter for feeds that have deviated sufficiently from their respective on-chain values, or where + // the on-chain values are sufficiently stale. + function checkCallback( + bytes[] memory values, + bytes memory lookupData + ) external view override returns (bool, bytes memory) { + bytes[] memory filteredValues = new bytes[](values.length); + uint256 count = 0; + for (uint256 i = 0; i < values.length; i++) { + Report memory report = getReport(values[i]); + string memory feedId = bytes32ToHexString(abi.encodePacked(report.feedId)); + Feed memory feed = s_feedMapping[feedId]; + if ( + (report.observationsTimestamp - feed.observationsTimestamp > feed.stalenessSeconds) || + deviationExceedsThreshold(feed.price, report.price, feed.deviationPercentagePPM) + ) { + filteredValues[count] = values[i]; + count++; + } + } + + // Adjusts the length of the filteredValues array to `count` such that it + // does not have extra empty slots, in case some items were filtered. + assembly { + mstore(filteredValues, count) + } + + bytes memory performData = abi.encode(filteredValues, lookupData); + return (filteredValues.length > 0, performData); + } + + // Use deviated off-chain values to update on-chain state. + function performUpkeep(bytes calldata performData) external override { + (bytes[] memory values /* bytes memory lookupData */, ) = abi.decode(performData, (bytes[], bytes)); + for (uint256 i = 0; i < values.length; i++) { + // Verify and decode the Mercury report. + Report memory report = abi.decode(s_verifier.verify(values[i]), (Report)); + string memory feedId = bytes32ToHexString(abi.encodePacked(report.feedId)); + + // Feeds that have been removed between checkUpkeep and performUpkeep should not be updated. + if (!s_feedMapping[feedId].active) { + revert FeedNotActive(feedId); + } + + // Ensure stale reports do not cause a regression in the registry. + if (s_feedMapping[feedId].observationsTimestamp > report.observationsTimestamp) { + revert StaleReport(feedId, s_feedMapping[feedId].observationsTimestamp, report.observationsTimestamp); + } + + // Assign new values to state. + s_feedMapping[feedId].bid = report.bid; + s_feedMapping[feedId].ask = report.ask; + s_feedMapping[feedId].price = report.price; + s_feedMapping[feedId].observationsTimestamp = report.observationsTimestamp; + + // Emit log. + emit FeedUpdated(report.observationsTimestamp, report.price, report.bid, report.ask, feedId); + + // Ensure enough gas remains for the next iteration. Otherwise, stop here. + if (gasleft() < MIN_GAS_FOR_PERFORM) { + return; + } + } + } + + // Decodes a mercury respone into an on-chain object. Thanks @mikestone!! + function getReport(bytes memory signedReport) internal pure returns (Report memory) { + /* + * bytes32[3] memory reportContext, + * bytes memory reportData, + * bytes32[] memory rs, + * bytes32[] memory ss, + * bytes32 rawVs + **/ + (, bytes memory reportData, , , ) = abi.decode(signedReport, (bytes32[3], bytes, bytes32[], bytes32[], bytes32)); + + Report memory report = abi.decode(reportData, (Report)); + return report; + } + + // Check if the off-chain value has deviated sufficiently from the on-chain value to justify an update. + // `scale` is used to ensure precision is not lost. + function deviationExceedsThreshold( + int192 onChain, + int192 offChain, + int192 deviationPercentagePPM + ) public pure returns (bool) { + // Compute absolute difference between the on-chain and off-chain values. + int192 scaledDifference = (onChain - offChain) * scale; + if (scaledDifference < 0) { + scaledDifference = -scaledDifference; + } + + // Compare to the allowed deviation from the on-chain value. + int192 deviationMax = ((onChain * scale) * deviationPercentagePPM) / scale; + return scaledDifference > deviationMax; + } + + // Helper function to reconcile a difference in formatting: + // - Automation passes feedId into their off-chain lookup function as a string. + // - Mercury stores feedId in their reports as a bytes32. + function bytes32ToHexString(bytes memory buffer) internal pure returns (string memory) { + bytes memory converted = new bytes(buffer.length * 2); + bytes memory _base = "0123456789abcdef"; + for (uint256 i = 0; i < buffer.length; i++) { + converted[i * 2] = _base[uint8(buffer[i]) / _base.length]; + converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length]; + } + return string(abi.encodePacked("0x", converted)); + } + + function addFeeds( + string[] memory feedIds, + string[] memory feedNames, + int192[] memory deviationPercentagePPMs, + uint32[] memory stalenessSeconds + ) external onlyOwner feedsAreValid(feedIds, feedNames, deviationPercentagePPMs, stalenessSeconds) { + for (uint256 i = 0; i < feedIds.length; i++) { + string memory feedId = feedIds[i]; + if (s_feedMapping[feedId].active) { + revert DuplicateFeed(feedId); + } + updateFeed(feedId, feedNames[i], deviationPercentagePPMs[i], stalenessSeconds[i]); + s_feedMapping[feedId].active = true; + + s_feeds.push(feedId); + } + } + + function setFeeds( + string[] memory feedIds, + string[] memory feedNames, + int192[] memory deviationPercentagePPMs, + uint32[] memory stalenessSeconds + ) public onlyOwner feedsAreValid(feedIds, feedNames, deviationPercentagePPMs, stalenessSeconds) { + // Clear prior feeds. + for (uint256 i = 0; i < s_feeds.length; i++) { + s_feedMapping[s_feeds[i]].active = false; + } + + // Assign new feeds. + for (uint256 i = 0; i < feedIds.length; i++) { + string memory feedId = feedIds[i]; + if (s_feedMapping[feedId].active) { + revert DuplicateFeed(feedId); + } + updateFeed(feedId, feedNames[i], deviationPercentagePPMs[i], stalenessSeconds[i]); + s_feedMapping[feedId].active = true; + } + s_feeds = feedIds; + } + + function updateFeed( + string memory feedId, + string memory feedName, + int192 deviationPercentagePPM, + uint32 stalnessSeconds + ) internal { + s_feedMapping[feedId].feedName = feedName; + s_feedMapping[feedId].deviationPercentagePPM = deviationPercentagePPM; + s_feedMapping[feedId].stalenessSeconds = stalnessSeconds; + s_feedMapping[feedId].feedId = feedId; + } + + function setVerifier(address verifier) external onlyOwner { + s_verifier = IVerifierProxy(verifier); + } + + modifier feedsAreValid( + string[] memory feedIds, + string[] memory feedNames, + int192[] memory deviationPercentagePPMs, + uint32[] memory stalenessSeconds + ) { + if (feedIds.length != feedNames.length) { + revert InvalidFeeds(); + } + if (feedIds.length != deviationPercentagePPMs.length) { + revert InvalidFeeds(); + } + if (feedIds.length != stalenessSeconds.length) { + revert InvalidFeeds(); + } + _; + } +} + +interface IVerifierProxy { + /** + * @notice Verifies that the data encoded has been signed + * correctly by routing to the correct verifier, and bills the user if applicable. + * @param payload The encoded data to be verified, including the signed + * report and any metadata for billing. + * @return verifiedReport The encoded report from the verifier. + */ + function verify(bytes calldata payload) external payable returns (bytes memory verifiedReport); +} diff --git a/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol b/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol new file mode 100644 index 00000000000..416b68f3a79 --- /dev/null +++ b/contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol @@ -0,0 +1,80 @@ +pragma solidity 0.8.6; + +import "../../shared/access/ConfirmedOwner.sol"; +import "../interfaces/AutomationCompatibleInterface.sol"; +import "../interfaces/StreamsLookupCompatibleInterface.sol"; +import "./MercuryRegistry.sol"; + +contract MercuryRegistryBatchUpkeep is ConfirmedOwner, AutomationCompatibleInterface, StreamsLookupCompatibleInterface { + error BatchSizeTooLarge(uint256 batchsize, uint256 maxBatchSize); + // Use a reasonable maximum batch size. Every Mercury report is ~750 bytes, too many reports + // passed into a single batch could exceed the calldata or transaction size limit for some blockchains. + uint256 constant MAX_BATCH_SIZE = 50; + + MercuryRegistry immutable i_registry; // master registry, where feed data is stored + + uint256 s_batchStart; // starting index of upkeep batch on the MercuryRegistry's s_feeds array, inclusive + uint256 s_batchEnd; // ending index of upkeep batch on the MercuryRegistry's s_feeds array, exclusive + + constructor(address mercuryRegistry, uint256 batchStart, uint256 batchEnd) ConfirmedOwner(msg.sender) { + i_registry = MercuryRegistry(mercuryRegistry); + + updateBatchingWindow(batchStart, batchEnd); + } + + // Invoke a feed lookup for the feeds this upkeep is responsible for. + function checkUpkeep(bytes calldata /* data */) external view override returns (bool, bytes memory) { + uint256 start = s_batchStart; + uint256 end = s_batchEnd; + string[] memory feeds = new string[](end - start); + uint256 count = 0; + for (uint256 i = start; i < end; i++) { + string memory feedId; + + // If the feed doesn't exist, then the batching window exceeds the underlying registry length. + // So, the batch will be partially empty. + try i_registry.s_feeds(i) returns (string memory f) { + feedId = f; + } catch (bytes memory /* data */) { + break; + } + + // Assign feed. + feeds[i - start] = feedId; + count++; + } + + // Adjusts the length of the batch to `count` such that it does not + // contain any empty feed Ids. + assembly { + mstore(feeds, count) + } + + return i_registry.revertForFeedLookup(feeds); + } + + // Use the master registry to assess deviations. + function checkCallback( + bytes[] memory values, + bytes memory lookupData + ) external view override returns (bool, bytes memory) { + return i_registry.checkCallback(values, lookupData); + } + + // Use the master registry to update state. + function performUpkeep(bytes calldata performData) external override { + i_registry.performUpkeep(performData); + } + + function updateBatchingWindow(uint256 batchStart, uint256 batchEnd) public onlyOwner { + // Do not allow a batched mercury registry to use an excessive batch size, as to avoid + // calldata size limits. If more feeds need to be updated than allowed by the batch size, + // deploy another `MercuryRegistryBatchUpkeep` contract and register another upkeep job. + if (batchEnd - batchStart > MAX_BATCH_SIZE) { + revert BatchSizeTooLarge(batchEnd - batchStart, MAX_BATCH_SIZE); + } + + s_batchStart = batchStart; + s_batchEnd = batchEnd; + } +} diff --git a/contracts/src/v0.8/automation/test/MercuryRegistry.t.sol b/contracts/src/v0.8/automation/test/MercuryRegistry.t.sol new file mode 100644 index 00000000000..4018c769f9a --- /dev/null +++ b/contracts/src/v0.8/automation/test/MercuryRegistry.t.sol @@ -0,0 +1,336 @@ +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import "../dev/MercuryRegistry.sol"; +import "../dev/MercuryRegistryBatchUpkeep.sol"; +import "../interfaces/StreamsLookupCompatibleInterface.sol"; + +contract MercuryRegistryTest is Test { + address internal constant OWNER = 0x00007e64E1fB0C487F25dd6D3601ff6aF8d32e4e; + int192 internal constant DEVIATION_THRESHOLD = 10_000; // 1% + uint32 internal constant STALENESS_SECONDS = 3600; // 1 hour + + address s_verifier = 0x60448B880c9f3B501af3f343DA9284148BD7D77C; + + string[] feedIds; + string s_BTCUSDFeedId = "0x6962e629c3a0f5b7e3e9294b0c283c9b20f94f1c89c8ba8c1ee4650738f20fb2"; + string s_ETHUSDFeedId = "0xf753e1201d54ac94dfd9334c542562ff7e42993419a661261d010af0cbfd4e34"; + MercuryRegistry s_testRegistry; + + // Feed: BTC/USD + // Date: Tuesday, August 22, 2023 7:29:28 PM + // Price: $25,857.11126720 + bytes s_august22BTCUSDMercuryReport = + hex"0006a2f7f9b6c10385739c687064aa1e457812927f59446cccddf7740cc025ad00000000000000000000000000000000000000000000000000000000014cb94e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000280010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001206962e629c3a0f5b7e3e9294b0c283c9b20f94f1c89c8ba8c1ee4650738f20fb20000000000000000000000000000000000000000000000000000000064e50c980000000000000000000000000000000000000000000000000000025a0864a8c00000000000000000000000000000000000000000000000000000025a063481720000000000000000000000000000000000000000000000000000025a0a94d00f000000000000000000000000000000000000000000000000000000000226181f4733a6d98892d1821771c041d5d69298210fdca9d643ad74477423b6a3045647000000000000000000000000000000000000000000000000000000000226181f0000000000000000000000000000000000000000000000000000000064e50c9700000000000000000000000000000000000000000000000000000000000000027f3056b1b71dd516037afd2e636f8afb39853f5cb3ccaa4b02d6f9a2a64622534e94aa1f794f6a72478deb7e0eb2942864b7fac76d6e120bd809530b1b74a32b00000000000000000000000000000000000000000000000000000000000000027bd3b385c0812dfcad2652d225410a014a0b836cd9635a6e7fb404f65f7a912f0b193db57e5c4f38ce71f29170f7eadfa94d972338858bacd59ab224245206db"; + + // Feed: BTC/USD + // Date: Wednesday, August 23, 2023 7:55:02 PM + // Price: $26,720.37346975 + bytes s_august23BTCUSDMercuryReport = + hex"0006a2f7f9b6c10385739c687064aa1e457812927f59446cccddf7740cc025ad000000000000000000000000000000000000000000000000000000000159a630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001206962e629c3a0f5b7e3e9294b0c283c9b20f94f1c89c8ba8c1ee4650738f20fb20000000000000000000000000000000000000000000000000000000064e664160000000000000000000000000000000000000000000000000000026e21d63e9f0000000000000000000000000000000000000000000000000000026e2147576a0000000000000000000000000000000000000000000000000000026e226525d30000000000000000000000000000000000000000000000000000000002286ce7c44fa27f67f6dd0a8bb40c12f0f050231845789f022a82aa5f4b3fe5bf2068fb0000000000000000000000000000000000000000000000000000000002286ce70000000000000000000000000000000000000000000000000000000064e664150000000000000000000000000000000000000000000000000000000000000002e9c5857631172082a47a20aa2fd9f580c1c48275d030c17a2dff77da04f88708ce776ef74c04b9ef6ba87c56d8f8c57e80ddd5298b477d60dd49fb8120f1b9ce000000000000000000000000000000000000000000000000000000000000000248624e0e2341cdaf989098f8b3dee2660b792b24e5251d6e48e3abe0a879c0683163a3a199969010e15353a99926d113f6d4cbab9d82ae90a159af9f74f8c157"; + + // Feed: BTC/USD + // Date: Wednesday, August 23, 2023 8:13:28 PM + // Price: $26,559.67100000 + bytes s_august23BTCUSDMercuryReport_2 = + hex"0006a2f7f9b6c10385739c687064aa1e457812927f59446cccddf7740cc025ad000000000000000000000000000000000000000000000000000000000159d009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000280010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001206962e629c3a0f5b7e3e9294b0c283c9b20f94f1c89c8ba8c1ee4650738f20fb20000000000000000000000000000000000000000000000000000000064e668690000000000000000000000000000000000000000000000000000026a63f9bc600000000000000000000000000000000000000000000000000000026a635984c00000000000000000000000000000000000000000000000000000026a67bb929d00000000000000000000000000000000000000000000000000000000022873e999d3ff9b644bba530af933dfaa6c59e31c3e232fcaa1e5f7304e2e79d939da1900000000000000000000000000000000000000000000000000000000022873e80000000000000000000000000000000000000000000000000000000064e66868000000000000000000000000000000000000000000000000000000000000000247c21657a6c2795986e95081876bf8b5f24bf72abd2dc4c601e7c96d654bcf543b5bb730e3d4736a308095e4531e7c03f581ac364f0889922ba3ae24b7cf968000000000000000000000000000000000000000000000000000000000000000020d3037d9f55256a001a2aa79ea746526c7cb36747e1deb4c804311394b4027667e5b711bcecfe60632e86cf8e83c28d1465e2d8d90bc0638dad8347f55488e8e"; + + // Feed: ETH/USD + // Date: Wednesday, August 23, 2023 7:55:01 PM + // Price: $1,690.76482169 + bytes s_august23ETHUSDMercuryReport = + hex"0006c41ec94138ae62cce3f1a2b852e42fe70359502fa7b6bdbf81207970d88e00000000000000000000000000000000000000000000000000000000016d874d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120f753e1201d54ac94dfd9334c542562ff7e42993419a661261d010af0cbfd4e340000000000000000000000000000000000000000000000000000000064e66415000000000000000000000000000000000000000000000000000000275dbe6079000000000000000000000000000000000000000000000000000000275c905eba000000000000000000000000000000000000000000000000000000275e5693080000000000000000000000000000000000000000000000000000000002286ce7c44fa27f67f6dd0a8bb40c12f0f050231845789f022a82aa5f4b3fe5bf2068fb0000000000000000000000000000000000000000000000000000000002286ce70000000000000000000000000000000000000000000000000000000064e664150000000000000000000000000000000000000000000000000000000000000002a2b01f7741563cfe305efaec43e56cd85731e3a8e2396f7c625bd16adca7b39c97805b6170adc84d065f9d68c87104c3509aeefef42c0d1711e028ace633888000000000000000000000000000000000000000000000000000000000000000025d984ad476bda9547cf0f90d32732dc5a0d84b0e2fe9795149b786fb05332d4c092e278b4dddeef45c070b818c6e221db2633b573d616ef923c755a145ea099c"; + + // Feed: USDC/USD + // Date: Wednesday, August 30, 2023 5:05:01 PM + // Price: $1.00035464 + bytes s_august30USDCUSDMercuryReport = + hex"0006970c13551e2a390246f5eccb62b9be26848e72026830f4688f49201b5a050000000000000000000000000000000000000000000000000000000001c89843000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120a5b07943b89e2c278fc8a2754e2854316e03cb959f6d323c2d5da218fb6b0ff80000000000000000000000000000000000000000000000000000000064ef69fa0000000000000000000000000000000000000000000000000000000005f5da000000000000000000000000000000000000000000000000000000000005f5b0f80000000000000000000000000000000000000000000000000000000005f5f8b0000000000000000000000000000000000000000000000000000000000240057307d0a0421d25328cb6dcfc5d0e211ff0580baaaf104e9877fc52cf2e8ec0aa7d00000000000000000000000000000000000000000000000000000000024005730000000000000000000000000000000000000000000000000000000064ef69fa0000000000000000000000000000000000000000000000000000000000000002b9e7fb46f1e9d22a1156024dc2bbf2bc6d337e0a2d78aaa3fb6e43b880217e5897732b516e39074ef4dcda488733bfee80c0a10714b94621cd93df6842373cf5000000000000000000000000000000000000000000000000000000000000000205ca5f8da9d6ae01ec6d85c681e536043323405b3b8a15e4d2a288e02dac32f10b2294593e270a4bbf53b0c4978b725293e85e49685f1d3ce915ff670ab6612f"; + + function setUp() public virtual { + // Set owner, and fork Arbitrum Goerli Testnet (chain ID 421613). + // The fork is only used with the `FORK_TEST` flag enabeld, as to not disrupt CI. For CI, a mock verifier is used instead. + vm.startPrank(OWNER); + try vm.envBool("FORK_TEST") returns (bool /* fork testing enabled */) { + vm.selectFork(vm.createFork("https://goerli-rollup.arbitrum.io/rpc")); + } catch { + s_verifier = address(new MockVerifierProxy()); + } + vm.chainId(31337); // restore chain Id + + // Use a BTC feed and ETH feed. + feedIds = new string[](2); + feedIds[0] = s_BTCUSDFeedId; + feedIds[1] = s_ETHUSDFeedId; + + // Deviation threshold and staleness are the same for all feeds. + int192[] memory thresholds = new int192[](1); + thresholds[0] = DEVIATION_THRESHOLD; + uint32[] memory stalenessSeconds = new uint32[](1); + stalenessSeconds[0] = STALENESS_SECONDS; + + // Initialize with BTC feed. + string[] memory initialFeedIds = new string[](1); + initialFeedIds[0] = feedIds[0]; + string[] memory initialFeedNames = new string[](1); + initialFeedNames[0] = "BTC/USD"; + s_testRegistry = new MercuryRegistry( + initialFeedIds, + initialFeedNames, + thresholds, + stalenessSeconds, + address(0) // verifier unset + ); + s_testRegistry.setVerifier(s_verifier); // set verifier + + // Add ETH feed. + string[] memory addedFeedIds = new string[](1); + addedFeedIds[0] = feedIds[1]; + string[] memory addedFeedNames = new string[](1); + addedFeedNames[0] = "ETH/USD"; + s_testRegistry.addFeeds(addedFeedIds, addedFeedNames, thresholds, stalenessSeconds); + } + + function testMercuryRegistry() public { + // Check upkeep, receive Mercury revert. + uint256 blockNumber = block.number; + vm.expectRevert( + abi.encodeWithSelector( + StreamsLookupCompatibleInterface.StreamsLookup.selector, + "feedIdHex", // feedParamKey + feedIds, // feed Ids + "blockNumber", // timeParamKey + blockNumber, // block number on which request is occuring + "" // extra data + ) + ); + s_testRegistry.checkUpkeep(""); + + // Obtain mercury report off-chain (for August 22 BTC/USD price) + bytes[] memory values = new bytes[](1); + values[0] = s_august22BTCUSDMercuryReport; + + // Pass the obtained mercury report into checkCallback, to assert that an update is warranted. + (bool shouldPerformUpkeep, bytes memory performData) = s_testRegistry.checkCallback(values, bytes("")); + assertEq(shouldPerformUpkeep, true); + + // Perform upkeep to update on-chain state. + s_testRegistry.performUpkeep(performData); + + // Check state of BTC/USD feed to ensure update was propagated. + bytes memory oldPerformData; + uint32 oldObservationsTimestamp; + { + // scoped to prevent stack-too-deep error + ( + uint32 observationsTimestamp, + int192 price, + int192 bid, + int192 ask, + string memory feedName, + string memory localFeedId, + bool active, + int192 deviationPercentagePPM, + uint32 stalenessSeconds + ) = s_testRegistry.s_feedMapping(s_BTCUSDFeedId); + assertEq(observationsTimestamp, 1692732568); // Tuesday, August 22, 2023 7:29:28 PM + assertEq(bid, 2585674416498); // $25,856.74416498 + assertEq(price, 2585711126720); // $25,857.11126720 + assertEq(ask, 2585747836943); // $25,857.47836943 + assertEq(feedName, "BTC/USD"); + assertEq(localFeedId, s_BTCUSDFeedId); + assertEq(active, true); + assertEq(deviationPercentagePPM, DEVIATION_THRESHOLD); + assertEq(stalenessSeconds, STALENESS_SECONDS); + + // Save this for later in the test. + oldPerformData = performData; + oldObservationsTimestamp = observationsTimestamp; + } + // Obtain mercury report off-chain (for August 23 BTC/USD price & ETH/USD price) + values = new bytes[](2); + values[0] = s_august23BTCUSDMercuryReport; + values[1] = s_august23ETHUSDMercuryReport; + + // Pass the obtained mercury report into checkCallback, to assert that an update is warranted. + (shouldPerformUpkeep, performData) = s_testRegistry.checkCallback(values, bytes("")); + assertEq(shouldPerformUpkeep, true); + + // Perform upkeep to update on-chain state. + s_testRegistry.performUpkeep(performData); + + // Make a batch request for both the BTC/USD feed data and the ETH/USD feed data. + MercuryRegistry.Feed[] memory feeds = s_testRegistry.getLatestFeedData(feedIds); + + // Check state of BTC/USD feed to ensure update was propagated. + assertEq(feeds[0].observationsTimestamp, 1692820502); // Wednesday, August 23, 2023 7:55:02 PM + assertEq(feeds[0].bid, 2672027981674); // $26,720.27981674 + assertEq(feeds[0].price, 2672037346975); // $26,720.37346975 + assertEq(feeds[0].ask, 2672046712275); // $26,720.46712275 + assertEq(feeds[0].feedName, "BTC/USD"); + assertEq(feeds[0].feedId, s_BTCUSDFeedId); + + // Check state of ETH/USD feed to ensure update was propagated. + assertEq(feeds[1].observationsTimestamp, 1692820501); // Wednesday, August 23, 2023 7:55:01 PM + assertEq(feeds[1].bid, 169056689850); // $1,690.56689850 + assertEq(feeds[1].price, 169076482169); // $1,690.76482169 + assertEq(feeds[1].ask, 169086456584); // $16,90.86456584 + assertEq(feeds[1].feedName, "ETH/USD"); + assertEq(feeds[1].feedId, s_ETHUSDFeedId); + assertEq(feeds[1].active, true); + assertEq(feeds[1].deviationPercentagePPM, DEVIATION_THRESHOLD); + assertEq(feeds[1].stalenessSeconds, STALENESS_SECONDS); + + // Obtain mercury report off-chain for August 23 BTC/USD price (second report of the day). + // The price of this incoming report will not deviate enough from the on-chain value to trigger an update, + // nor is the on-chain data stale enough. + values = new bytes[](1); + values[0] = s_august23BTCUSDMercuryReport_2; + + // Pass the obtained mercury report into checkCallback, to assert that an update is not warranted. + (shouldPerformUpkeep, performData) = s_testRegistry.checkCallback(values, bytes("")); + assertEq(shouldPerformUpkeep, false); + + // Ensure stale reports cannot be included. + vm.expectRevert( + abi.encodeWithSelector( + MercuryRegistry.StaleReport.selector, + feedIds[0], + feeds[0].observationsTimestamp, + oldObservationsTimestamp + ) + ); + s_testRegistry.performUpkeep(oldPerformData); + + // Ensure reports for inactive feeds cannot be included. + bytes[] memory inactiveFeedReports = new bytes[](1); + inactiveFeedReports[0] = s_august30USDCUSDMercuryReport; + bytes memory lookupData = ""; + vm.expectRevert( + abi.encodeWithSelector( + MercuryRegistry.FeedNotActive.selector, + "0xa5b07943b89e2c278fc8a2754e2854316e03cb959f6d323c2d5da218fb6b0ff8" // USDC/USD feed id + ) + ); + s_testRegistry.performUpkeep(abi.encode(inactiveFeedReports, lookupData)); + } + + // Below are the same tests as `testMercuryRegistry`, except done via a batching Mercury registry that + // consumes the test registry. This is to assert that batching can be accomplished by multiple different + // upkeep jobs, which can populate the same + function testMercuryRegistryBatchUpkeep() public { + MercuryRegistryBatchUpkeep batchedRegistry = new MercuryRegistryBatchUpkeep( + address(s_testRegistry), // use the test registry as master registry + 0, // start batch at index 0. + 50 // end batch beyond length of feed Ids (take responsibility for all feeds) + ); + // Check upkeep, receive Mercury revert. + uint256 blockNumber = block.number; + vm.expectRevert( + abi.encodeWithSelector( + StreamsLookupCompatibleInterface.StreamsLookup.selector, + "feedIdHex", // feedParamKey + feedIds, // feed Ids + "blockNumber", // timeParamKey + blockNumber, // block number on which request is occuring + "" // extra data + ) + ); + batchedRegistry.checkUpkeep(""); + + // Obtain mercury report off-chain (for August 22 BTC/USD price) + bytes[] memory values = new bytes[](1); + values[0] = s_august22BTCUSDMercuryReport; + + // Pass the obtained mercury report into checkCallback, to assert that an update is warranted. + (bool shouldPerformUpkeep, bytes memory performData) = batchedRegistry.checkCallback(values, bytes("")); + assertEq(shouldPerformUpkeep, true); + + // Perform upkeep to update on-chain state. + batchedRegistry.performUpkeep(performData); + + // Check state of BTC/USD feed to ensure update was propagated. + ( + uint32 observationsTimestamp, + int192 price, + int192 bid, + int192 ask, + string memory feedName, + string memory localFeedId, + bool active, + int192 deviationPercentagePPM, + uint32 stalenessSeconds + ) = s_testRegistry.s_feedMapping(s_BTCUSDFeedId); + assertEq(observationsTimestamp, 1692732568); // Tuesday, August 22, 2023 7:29:28 PM + assertEq(bid, 2585674416498); // $25,856.74416498 + assertEq(price, 2585711126720); // $25,857.11126720 + assertEq(ask, 2585747836943); // $25,857.47836943 + assertEq(feedName, "BTC/USD"); + assertEq(localFeedId, s_BTCUSDFeedId); + assertEq(active, true); + assertEq(deviationPercentagePPM, DEVIATION_THRESHOLD); + assertEq(stalenessSeconds, STALENESS_SECONDS); + + // Obtain mercury report off-chain (for August 23 BTC/USD price & ETH/USD price) + values = new bytes[](2); + values[0] = s_august23BTCUSDMercuryReport; + values[1] = s_august23ETHUSDMercuryReport; + + // Pass the obtained mercury report into checkCallback, to assert that an update is warranted. + (shouldPerformUpkeep, performData) = batchedRegistry.checkCallback(values, bytes("")); + assertEq(shouldPerformUpkeep, true); + + // Perform upkeep to update on-chain state, but with not enough gas to update both feeds. + batchedRegistry.performUpkeep{gas: 250_000}(performData); + + // Make a batch request for both the BTC/USD feed data and the ETH/USD feed data. + MercuryRegistry.Feed[] memory feeds = s_testRegistry.getLatestFeedData(feedIds); + + // Check state of BTC/USD feed to ensure update was propagated. + assertEq(feeds[0].observationsTimestamp, 1692820502); // Wednesday, August 23, 2023 7:55:02 PM + assertEq(feeds[0].bid, 2672027981674); // $26,720.27981674 + assertEq(feeds[0].price, 2672037346975); // $26,720.37346975 + assertEq(feeds[0].ask, 2672046712275); // $26,720.46712275 + assertEq(feeds[0].feedName, "BTC/USD"); + assertEq(feeds[0].feedId, s_BTCUSDFeedId); + + // Check state of ETH/USD feed to observe that the update was not propagated. + assertEq(feeds[1].observationsTimestamp, 0); + assertEq(feeds[1].bid, 0); + assertEq(feeds[1].price, 0); + assertEq(feeds[1].ask, 0); + assertEq(feeds[1].feedName, "ETH/USD"); + assertEq(feeds[1].feedId, s_ETHUSDFeedId); + assertEq(feeds[1].active, true); + assertEq(feeds[1].deviationPercentagePPM, DEVIATION_THRESHOLD); + assertEq(feeds[1].stalenessSeconds, STALENESS_SECONDS); + + // Try again, with sufficient gas to update both feeds. + batchedRegistry.performUpkeep{gas: 2_500_000}(performData); + feeds = s_testRegistry.getLatestFeedData(feedIds); + + // Check state of ETH/USD feed to ensure update was propagated. + assertEq(feeds[1].observationsTimestamp, 1692820501); // Wednesday, August 23, 2023 7:55:01 PM + assertEq(feeds[1].bid, 169056689850); // $1,690.56689850 + assertEq(feeds[1].price, 169076482169); // $1,690.76482169 + assertEq(feeds[1].ask, 169086456584); // $16,90.86456584 + assertEq(feeds[1].feedName, "ETH/USD"); + assertEq(feeds[1].feedId, s_ETHUSDFeedId); + + // Obtain mercury report off-chain for August 23 BTC/USD price (second report of the day). + // The price of this incoming report will not deviate enough from the on-chain value to trigger an update. + values = new bytes[](1); + values[0] = s_august23BTCUSDMercuryReport_2; + + // Pass the obtained mercury report into checkCallback, to assert that an update is not warranted. + (shouldPerformUpkeep, performData) = batchedRegistry.checkCallback(values, bytes("")); + assertEq(shouldPerformUpkeep, false); + } +} + +contract MockVerifierProxy is IVerifierProxy { + function verify(bytes calldata payload) external payable override returns (bytes memory) { + (, bytes memory reportData, , , ) = abi.decode(payload, (bytes32[3], bytes, bytes32[], bytes32[], bytes32)); + return reportData; + } +}