Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VRF-567] Mercury price feed registry #10348

Merged
merged 37 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
283c4c3
[VRF-567] Mercury price feed registry
vreff Aug 24, 2023
71e87f8
prettier
vreff Aug 24, 2023
c8a56ca
Add set/add feed functionality
vreff Aug 28, 2023
b8c82d9
Add configuration functions
vreff Aug 28, 2023
67e762b
Use the result of 'verify'
vreff Aug 28, 2023
2697c03
Fix typo
vreff Aug 28, 2023
5024dc2
Add access control and extra tests, fix chainspecific util, and make …
vreff Aug 30, 2023
6b933fb
Use different URL for fork.
vreff Aug 30, 2023
1715b24
Add snapshot
vreff Aug 30, 2023
5b0ab7c
Ensure batch upkeep doesn't revert, and add permissions for it
vreff Aug 30, 2023
896785b
Fix typos, update snapshots
vreff Aug 30, 2023
28c232b
Update contracts/src/v0.8/dev/automation/upkeeps/MercuryRegistry.sol
vreff Aug 31, 2023
f511940
Update contracts/src/v0.8/dev/automation/upkeeps/MercuryRegistry.sol
vreff Aug 31, 2023
96f04a0
Update documentation, and adjust test to use forked block only for CI
vreff Aug 31, 2023
9485c2b
Add documentation for caching behavior
vreff Aug 31, 2023
4c51cf9
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 1, 2023
ffef470
Update interface from feedlookup to streamslookup
vreff Sep 1, 2023
d91701f
Remove inaccurate lookup data in test
vreff Sep 1, 2023
b537c1d
Update gas snapshot
vreff Sep 1, 2023
b2808f9
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 1, 2023
d65a3bb
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 1, 2023
883934d
Remove fork testing in CI
vreff Sep 1, 2023
b95cca9
Make staleness seconds and deviation PPM feed-specific
vreff Sep 1, 2023
c9a8220
Replace text require with custom error for feed lenght check
vreff Sep 1, 2023
3c3632a
Update gas snapshot
vreff Sep 1, 2023
98211bc
Prettier
vreff Sep 1, 2023
84562e0
Change flag restrition on fork testing
vreff Sep 1, 2023
1347e2b
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 5, 2023
eb5637a
Update automation-dev.gas-snapshot
vreff Sep 5, 2023
60f51e0
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 6, 2023
3b37f42
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 13, 2023
45458e0
Update file locations
vreff Sep 13, 2023
6320cff
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 13, 2023
98e03ee
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 19, 2023
bf78e9c
Add comment denoting the use of Mercury v0.2
vreff Sep 19, 2023
62ca3c3
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 20, 2023
2e9f0f3
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff Sep 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contracts/src/v0.8/ChainSpecificUtil.sol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this lib is great for arb!

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ library ChainSpecificUtil {
ArbSys private constant ARBSYS = ArbSys(ARBSYS_ADDR);
address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C);
ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR);
uint256 private constant ARB_MAINNET_CHAIN_ID = 42161;
uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613;
uint256 private constant ARB_MAINNET_CHAIN_ID = 0; // 42161, disabled for Forge testing
uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 0; // 421613, disabled for Forge testing

function getBlockhash(uint64 blockNumber) internal view returns (bytes32) {
uint256 chainid = block.chainid;
Expand Down
237 changes: 237 additions & 0 deletions contracts/src/v0.8/dev/automation/upkeeps/MercuryRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
pragma solidity 0.8.6;

import "../../../automation/interfaces/AutomationCompatibleInterface.sol";
import "../2_1/interfaces/FeedLookupCompatibleInterface.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. |
| - Scalabiliy. Feeds can be added or removed from the contract with a single contract call, and the number of |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo scalability

| 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.soll` - contains foundry tests to demonstrate various flows. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo soll

| |
| TODO: |
| - Convert s_feedIds to an enumerable set. |
| - Enable an owner to add and remove feeds after the contract is deployed. |
| - Access control. Specifically, the the ability to execute `performUpkeep`. |
| - Optimize gas consumption. |
-+---------------------------------------------------------------------------------------------------------------------*/
contract MercuryRegistry is AutomationCompatibleInterface, FeedLookupCompatibleInterface {
// 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)
}

// 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);

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 immutable i_verifier; // for Mercury v0.2 - verifies off-chain reports

int192 constant scale = 1_000_000; // a scalar used for measuring deviation with precision
int192 s_deviationPercentagePPM; // acceptable deviatoin threshold - 1.5% = 15_000, 100% = 1_000_000, etc..
uint32 s_stalenessSeconds; // acceptable staleness threshold - 60 = 1 minute, 300 = 5 minutes, etc..

string[] public s_feeds; // list of feed Ids
mapping(string => Feed) public s_feedMapping; // mapping of feed Ids to stored feed data
makramkd marked this conversation as resolved.
Show resolved Hide resolved

constructor(
vreff marked this conversation as resolved.
Show resolved Hide resolved
string[] memory feedIds,
string[] memory feedNames,
address verifier,
int192 deviationPercentagePPM,
uint32 stalenessSeconds
) {
i_verifier = IVerifierProxy(verifier);

// Ensure correctly formatted constructor arguments.
require(feedIds.length == feedNames.length, "incorrect constructor args");

// Store desired deviation threshold and staleness seconds.
s_deviationPercentagePPM = deviationPercentagePPM;
s_stalenessSeconds = stalenessSeconds;

// Store desired feeds.
s_feeds = feedIds;
for (uint256 i = 0; i < feedIds.length; i++) {
s_feedMapping[s_feeds[i]] = Feed({
feedName: feedNames[i],
feedId: feedIds[i],
price: 0,
bid: 0,
ask: 0,
observationsTimestamp: 0
});
}
}

// 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]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess no checking of whether this feed ID exists in the mapping - user just needs to check return data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the FeedID is not in the mapping, active will be false. There may still be data from prior updates, but they will ultimately become stale.

}

return feeds;
}

// Invoke a feed lookup through the checkUpkeep function. Expected to run on a chron schedule.
vreff marked this conversation as resolved.
Show resolved Hide resolved
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 FeedLookup(c_feedParamKey, feeds, c_timeParamKey, blockNumber, "EXTRA_DATA_FOR_FUTURE_FUNCTIONS_CALLS");
}

// 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 = bytes32ToHextString(abi.encodePacked(report.feedId));
Feed memory feed = s_feedMapping[feedId];
if (
(report.observationsTimestamp - feed.observationsTimestamp > s_stalenessSeconds) ||
deviationExceedsThreshold(feed.price, report.price)
) {
filteredValues[count] = values[i];
count++;
}
}

// Adjusts the lenght 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.
// TODO:
// - The implementation provided here is readable but crude. Remaining gas should be checked between iterations
// of the for-loop, and the failure of a single item should not cause the entire batch to revert.
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 report.
i_verifier.verify(values[i]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use the response from verify function

Report memory report = getReport(values[i]);
string memory feedId = bytes32ToHextString(abi.encodePacked(report.feedId));

// Feeds that have been removed between checkUpkeep and performUpkeep should not be updated.
makramkd marked this conversation as resolved.
Show resolved Hide resolved
require(bytes(s_feedMapping[feedId].feedId).length > 0, "feed removed");

// Sanity check. Stale reports should not get through, but ensure they do not cause a regression
// in the registry.
require(s_feedMapping[feedId].observationsTimestamp <= report.observationsTimestamp, "stale report");

// 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 (not gas efficient to do this for each update).
emit FeedUpdated(report.observationsTimestamp, report.price, report.bid, report.ask, feedId);
}
}

// 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) public view 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) * s_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 bytes32ToHextString(bytes memory buffer) internal pure returns (string memory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo Hext

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));
}
}

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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
pragma solidity 0.8.6;

import "../../../automation/interfaces/AutomationCompatibleInterface.sol";
import "../2_1/interfaces/FeedLookupCompatibleInterface.sol";
import "./MercuryRegistry.sol";

contract MercuryRegistryBatchUpkeep is AutomationCompatibleInterface, FeedLookupCompatibleInterface {
// 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.
vreff marked this conversation as resolved.
Show resolved Hide resolved
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, inclusive
uint256 s_batchEnd; // ending index of upkeep batch, exclusive
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem like indexes into the s_feeds array in the MercuryRegistry contract - maybe can state that more clearly in the comment here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do.


constructor(address mercuryRegistry, uint256 batchStart, uint256 batchEnd) {
i_registry = MercuryRegistry(mercuryRegistry);

// 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.
require(batchEnd - batchStart <= MAX_BATCH_SIZE, "batch size is too large");

s_batchStart = batchStart;
s_batchEnd = 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);
for (uint256 i = start; i < end; i++) {
feeds[i - start] = i_registry.s_feeds(i);
}
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);
}
}
Loading