-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
+728
−0
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
283c4c3
[VRF-567] Mercury price feed registry
vreff 71e87f8
prettier
vreff c8a56ca
Add set/add feed functionality
vreff b8c82d9
Add configuration functions
vreff 67e762b
Use the result of 'verify'
vreff 2697c03
Fix typo
vreff 5024dc2
Add access control and extra tests, fix chainspecific util, and make …
vreff 6b933fb
Use different URL for fork.
vreff 1715b24
Add snapshot
vreff 5b0ab7c
Ensure batch upkeep doesn't revert, and add permissions for it
vreff 896785b
Fix typos, update snapshots
vreff 28c232b
Update contracts/src/v0.8/dev/automation/upkeeps/MercuryRegistry.sol
vreff f511940
Update contracts/src/v0.8/dev/automation/upkeeps/MercuryRegistry.sol
vreff 96f04a0
Update documentation, and adjust test to use forked block only for CI
vreff 9485c2b
Add documentation for caching behavior
vreff 4c51cf9
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff ffef470
Update interface from feedlookup to streamslookup
vreff d91701f
Remove inaccurate lookup data in test
vreff b537c1d
Update gas snapshot
vreff b2808f9
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff d65a3bb
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff 883934d
Remove fork testing in CI
vreff b95cca9
Make staleness seconds and deviation PPM feed-specific
vreff c9a8220
Replace text require with custom error for feed lenght check
vreff 3c3632a
Update gas snapshot
vreff 98211bc
Prettier
vreff 84562e0
Change flag restrition on fork testing
vreff 1347e2b
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff eb5637a
Update automation-dev.gas-snapshot
vreff 60f51e0
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff 3b37f42
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff 45458e0
Update file locations
vreff 6320cff
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff 98e03ee
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff bf78e9c
Add comment denoting the use of Mercury v0.2
vreff 62ca3c3
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff 2e9f0f3
Merge branch 'develop' into feature/VRF-567-mercury-registry
vreff File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
80 changes: 80 additions & 0 deletions
80
contracts/src/v0.8/automation/dev/MercuryRegistryBatchUpkeep.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add a comment somewhere to indicate that this only works with mercury v0.2?
for mercury v0.3, certain feeds still have the same report schema but some are different (timestamp feeds)
and verify function will take different params..