Using MVR Feeds on EVM Chains
This guide explains how to use Multiple-Variable Response (MVR) feeds data in your consumer contracts on EVM chains using Solidity.
MVR feeds store multiple data points in a single byte array onchain. To consume this data in your contract:
- Obtain the proxy address and data structure:
- Find the
BundleAggregatorProxyaddress for the specific MVR feed you want to read on the SmartData Addresses page - Expand the "MVR Bundle Info" section to see the exact data structure, field types, and decimals
- Note these details as you'll need to match this structure exactly in your code
- Find the
- Call
latestBundle(): Retrieve the feed's latest onchain data as a bytes array. - Check data staleness: Use
latestBundleTimestamp()to compare against current time and verify it hasn't exceeded your maximum acceptable staleness threshold. - Decode the data: Convert the bytes array into the known struct type (as documented for each feed).
- Apply decimals (if needed): For numeric fields, scale the raw values by dividing by
10^decimals[i]to get the true numerical values. - Use in your dApp: Store or process the decoded values as required by your application.
Step-by-Step Example
Below is a step-by-step explanation, followed by a full example contract that ties everything together.
1. Define a Data Structure
Each MVR feed publishes data in a specific layout. For example, imagine an investment feed that reports:
struct Data {
uint256 netAssetValue; // e.g., 8 decimal places
uint256 assetsUnderManagement; // e.g., 8 decimal places
uint256 outstandingShares; // e.g., 2 decimal places
uint256 netIncomeExpenses; // e.g., 2 decimal places
bool openToNewInvestors; // boolean, no decimals
}
Your consumer contract must replicate this structure in the exact same order and with the same data types to decode the feed data correctly.
2. Import the IBundleAggregatorProxy Interface
Your contract will need to interact with the MVR feed's proxy contract. The IBundleAggregatorProxy interface provides the necessary functions:
latestBundle(): Retrieves the raw data bundle.bundleDecimals(): Retrieves the decimal places for numeric fields.latestBundleTimestamp(): Retrieves the timestamp of the last update.
Import it directly from the @chainlink/contracts library:
import {IBundleAggregatorProxy} from "@chainlink/contracts/src/v0.8/data-feeds/interfaces/IBundleAggregatorProxy.sol";
You will then use this interface to create an instance of the proxy in your consumer contract.
3. Validate Data Staleness
Before using the data, it's best practice to verify it has not become stale by checking the timestamp of the latest update against the current time. Stale data can lead to incorrect business decisions or vulnerabilities in your application.
Staleness checking examples:
Option 1: Simple boolean check (recommended)
function isDataFresh() public view returns (bool) {
uint256 lastUpdateTime = s_proxy.latestBundleTimestamp();
return (block.timestamp - lastUpdateTime) <= STALENESS_THRESHOLD;
}
// In your main function:
if (!isDataFresh()) {
revert StaleData(lastUpdateTime, block.timestamp, STALENESS_THRESHOLD);
}
Option 2: Direct validation
uint256 lastUpdateTime = s_proxy.latestBundleTimestamp();
require(block.timestamp - lastUpdateTime <= stalenessThreshold, "MVR feed data is stale");
Important: Don't use arbitrary values for staleness thresholds. The appropriate threshold should be determined by:
- Find the feed's heartbeat interval on the MVR Feeds Addresses page (click "Show more details")
- Set a threshold that aligns with this interval, usually the heartbeat plus a small buffer
- Consider your specific use case requirements (some applications may need very fresh data)
4. Add Error Handling and Safety Checks
For production contracts, include proper error handling with custom errors and safety checks:
// Custom errors for better debugging
error StaleData(uint256 lastUpdateTimestamp, uint256 blockTimestamp, uint256 threshold);
error InsufficientDecimals(uint256 expected, uint256 actual);
// Check that the decimals array has enough elements before accessing by index
if (decimals.length < 4) {
revert InsufficientDecimals(4, decimals.length);
}
Helper functions for testing and debugging:
isDataFresh(): Returns a simple true/false for data freshness (excellent for block explorer testing)getLatestBundleTimestamp(): Returns the timestamp of the most recent updatestoreDecimals(): Fetches and stores the decimals array for repeated use
5. Read and Decode the Feed Data
Use abi.decode to convert the returned bytes array into your Data struct:
bytes memory rawBundle = s_proxy.latestBundle();
Data memory decodedData = abi.decode(rawBundle, (Data));
This will populate each field in decodedData according to your struct definition.
6. Handle Decimals (if applicable)
Similar to how ETH has 18 decimals, numeric fields in MVR feeds are typically stored with fixed-point precision. This means you need to divide raw values by a power of 10 to get their true numerical values for accurate calculations.
The bundleDecimals() function returns an array that tells you how many decimal places each field uses. Each index in this array corresponds to a field in your struct (in the same order).
- Raw Value: What you get directly from the feed (e.g.,
1850000000for a value that represents18.5) - Actual Value: What the number actually represents mathematically after proper decimal scaling
- Conversion:
actualValue = rawValue / (10 ^ decimals)
For example, if your MVR feed has the following fields and decimals:
| Field | Raw Value | Decimals | Actual Value |
|---|---|---|---|
netAssetValue | 1850000000 | 8 | 18.5 |
assetsUnderManagement | 5000000000 | 8 | 50.0 |
outstandingShares | 12500 | 2 | 125.0 |
netIncomeExpenses | 7540 | 2 | 75.4 |
openToNewInvestors | true | 0 | true (no decimals) |
Then bundleDecimals() would return an array like [8, 8, 2, 2, 0].
In your code, you would convert the values like this:
// For netAssetValue with 8 decimals
uint256 scaledValue = rawValue / 10**8; // 1850000000 / 10^8 = 18 (decimals truncated)
Important:
- In Solidity, division with
uint256results in integer division with truncation of any fractional part. If your application needs to maintain decimal precision, store the raw values and perform decimal conversion in your frontend application. - Always confirm the actual decimal configuration from the feed's documentation (see MVR Feeds Addresses). Different feeds may use different decimal patterns or skip them entirely for non-numeric fields.
7. Store or Use the Decoded Values
Finally, you can store the decoded values in your contract state or process them right away.
Full Example: Consumer Contract
Below is a full example that demonstrates how to:
- Initialize the proxy in the constructor
- Validate data staleness
- Retrieve and decode the latest bundle
- Adjust numeric fields by the correct decimal factor
- Store both the raw onchain values and the scaled values
undefined
Key Points:
- Exact Order Matters: The struct fields and their types must match the feed's definition.
- Decimals: Use
bundleDecimals()to learn how to scale the numeric fields. Non-numeric fields (e.g., bool) do not need scaling. - Timestamps:
latestBundleTimestamp()returns the block timestamp of the last report and should be used to validate data staleness. - Staleness Threshold: Set appropriate staleness thresholds based on each feed's documented heartbeat interval, not arbitrary values.
- Error Handling: Use custom errors (
StaleData,InsufficientDecimals) for better debugging and include safety checks for array bounds. - Testing Functions: Implement helper functions like
isDataFresh()for easy testing and debugging. - No Historical Data: MVR feeds typically only store the latest data onchain. If you need historical data, you must capture it in your own contract or via an offchain indexer.