Solidity consumer example
This guide walks through a complete contract that accepts a signed DataLink report, verifies it on-chain, decodes it, checks it, and stores the price.
How verification flows
text
Your backend ──GET /api/v1/reports/latest──▶ signed payload
│
▼ (sends a transaction with the payload)
Your contract.updatePrice(payload)
│
▼ verify(payload, 0x)
VerifierProxy ──reads the configuration digest, forwards──▶ Verifier
│ checks the signatures
◀──────────────── report data (nine fields) ─────────────┘
│
▼
Your contract decodes, checks feed + freshness, stores the price- Your backend fetches the signed report with its API key (the key never goes on-chain).
- A transaction calls your contract with the
0x-prefixedpayload. - Your contract calls the VerifierProxy, which forwards the report to the
Verifier. The Verifier checks the signatures, emits
ReportVerifiedand returns the report data — or reverts. - Your contract decodes the report data and applies its own checks.
The contract
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// The one VerifierProxy function a consumer needs.
interface IVerifierProxy {
/// Verifies a signed DataLink report and returns its report data.
/// Reverts if the signatures or the report are not valid.
function verify(bytes calldata payload, bytes calldata parameterPayload)
external
payable
returns (bytes memory verifierResponse);
}
/// Example DataLink consumer: verifies a signed report on-chain, decodes it,
/// checks it is for the expected feed and recent enough, then stores the price.
/// EXAMPLE CODE — not audited. Review and test it before any real use.
contract DataLinkPriceConsumer {
/// The nine fields encoded in a DataLink report (schema v1), in order.
struct Report {
bytes32 feedId;
uint32 observationsTimestamp;
int192 benchmarkPrice;
int192 bid;
int192 ask;
uint64 currentBlockNum;
bytes32 currentBlockHash;
uint64 validFromBlockNum;
uint64 currentBlockTimestamp;
}
IVerifierProxy public immutable verifierProxy;
bytes32 public immutable feedId;
/// Price decimals of this feed (from its report schema). Stored for reference:
/// a price of 8157000000 with 8 decimals means 81.57.
uint8 public immutable decimals;
/// Oldest report accepted, in seconds.
uint256 public immutable maxReportAge;
int192 public latestPrice;
int192 public latestBid;
int192 public latestAsk;
uint32 public latestObservationsTimestamp;
event PriceUpdated(bytes32 indexed feedId, int192 price, uint32 observationsTimestamp);
error WrongFeed(bytes32 expected, bytes32 received);
error StaleReport(uint32 observationsTimestamp, uint256 currentTimestamp);
error NotNewerThanStored(uint32 observationsTimestamp, uint32 storedTimestamp);
constructor(address verifierProxy_, bytes32 feedId_, uint8 decimals_, uint256 maxReportAge_) {
verifierProxy = IVerifierProxy(verifierProxy_);
feedId = feedId_;
decimals = decimals_;
maxReportAge = maxReportAge_;
}
/// @param signedPayload The `payload` from GET /api/v1/reports/latest, with a 0x prefix.
function updatePrice(bytes calldata signedPayload) external {
// 1. Verify. Reverts unless the DataLink signers signed this exact report.
// parameterPayload is empty: no FeeManager is configured on the
// XDC Mainnet VerifierProxy, so no fee is paid.
bytes memory reportData = verifierProxy.verify(signedPayload, "");
// 2. Decode the verified report data into its nine fields.
Report memory report = abi.decode(reportData, (Report));
// 3. Only accept the feed this contract is for.
if (report.feedId != feedId) revert WrongFeed(feedId, report.feedId);
// 4. Only accept recent reports.
if (uint256(report.observationsTimestamp) + maxReportAge < block.timestamp) {
revert StaleReport(report.observationsTimestamp, block.timestamp);
}
// 5. Never replace a newer stored price with an older (or the same) report.
if (report.observationsTimestamp <= latestObservationsTimestamp) {
revert NotNewerThanStored(report.observationsTimestamp, latestObservationsTimestamp);
}
// 6. Store.
latestPrice = report.benchmarkPrice;
latestBid = report.bid;
latestAsk = report.ask;
latestObservationsTimestamp = report.observationsTimestamp;
emit PriceUpdated(report.feedId, report.benchmarkPrice, report.observationsTimestamp);
}
}What each check does
| Step | Why |
|---|---|
verify | Only reports signed by the DataLink oracle network pass. Any changed byte reverts. |
abi.decode | Turns the verified report data into typed fields — the struct must match the report schema exactly. |
| Feed ID | A valid report for a different feed is still validly signed. Without this check, anyone could feed your contract another asset's price. |
| Freshness | A valid report stays valid forever. maxReportAge stops an old (but genuine) report from being used. Compare observationsTimestamp — not the API's createdAt, which isn't signed. |
| Newer than stored | Stops someone from replacing your latest price with an older report that is still within maxReportAge. |
Using the price
latestPrice is an integer with the feed's decimals. With 8 decimals, divide by
10^8: 8157000000 → 81.57. In Solidity, keep working in the integer form
(scale other values up rather than dividing), or convert to your own fixed-point
format.
Deploying and using it
-
Constructor arguments:
Argument Value verifierProxy_0xCf49c434f2206Df1D507dE9f5DA70cA8c3dA61d2(XDC Mainnet)feedId_Your feed's ID, e.g. 0x00018cd7a1e01e764604c0e299f74a0ff4179d81e92111389e6585ba6318cbd1decimals_The feed's price decimals (Feed Access page, or schemain the decoded API)maxReportAge_Seconds, e.g. 3600 -
Deploy with Remix (Environment: Injected Provider on XDC Mainnet), Hardhat or Foundry.
-
Update the price: your backend fetches a fresh report from
/api/v1/reports/latest, adds0xtopayload, and sendsupdatePrice(payload). -
Read
latestPrice,latestBid,latestAskandlatestObservationsTimestampfrom your contract.
In the receipt you'll see two events: ReportVerified from the Verifier (with
your contract as requester) and PriceUpdated from your contract.
Gas
Verifying through the VerifierProxy costs roughly 90,000 gas on its own (88,258 in
the example transaction); this contract's full updatePrice used about 145,000
gas in testing. Actual gas varies — let your wallet estimate it.
