Verify report data onchain

Guide Versions

This guide is available in multiple versions. Choose the one that matches your needs.

In this guide, you'll learn how to verify onchain the integrity of reports by confirming their authenticity as signed by the Decentralized Oracle Network (DON). You'll use a verifier contract to verify the data onchain and pay the verification fee in LINK tokens.

Before you begin

Make sure you understand how to use the Streams Direct implementation of Chainlink Data Streams to fetch reports via the REST API or WebSocket connection. Refer to the following guides for more information:

Requirements

Tutorial

Deploy the verifier contract

Deploy a ClientReportsVerifier contract on Arbitrum Sepolia. This contract is enabled to verify reports and pay the verification fee in LINK tokens.

  1. Open the ClientReportsVerifier.sol contract in Remix.

  2. Select the ClientReportsVerifier.sol contract in the Solidity Compiler tab.

    Chainlink Data Streams - Verify Report Data Onchain - Solidity Compiler
  3. Compile the contract.

  4. Open MetaMask and set the network to Arbitrum Sepolia. If you need to add Arbitrum Sepolia to your wallet, you can find the chain ID and the LINK token contract address on the LINK Token Contracts page.

  5. On the Deploy & Run Transactions tab in Remix, select Injected Provider - MetaMask in the Environment list. Remix will use the MetaMask wallet to communicate with Arbitrum Sepolia.

    Chainlink Data Streams - Verify Report Data Onchain - Injected Provider MetaMask
  6. In the Contract section, select the ClientReportsVerifier contract and fill in the Arbitrum Sepolia verifier proxy address: 0x2ff010DEbC1297f19579B4246cad07bd24F2488A. You can find the verifier proxy addresses on the Stream Addresses page.

    Chainlink Data Streams Remix Deploy ClientReportsVerifier Contract
  7. Click the Deploy button to deploy the contract. MetaMask prompts you to confirm the transaction. Check the transaction details to ensure you deploy the contract to Arbitrum Sepolia.

  8. After you confirm the transaction, the contract address appears under the Deployed Contracts list in Remix. Save this contract address for the next step.

    Chainlink Data Streams Remix Deployed ClientReportsVerifier Contract

Fund the verifier contract

In this example, the client contract pays for onchain verification of reports in LINK tokens when fees are required. The contract automatically detects whether the target network requires fees:

  • Networks with FeeManager deployed: Verification requires token payments. These networks include: Arbitrum, Avalanche, Base, Blast, Bob, Ink, Linea, OP, Scroll, Soneium, and ZKSync.

  • Networks without FeeManager: No funding is needed since you can verify reports without fees. The contract skips the fee calculation and approval steps.

For this tutorial on Arbitrum Sepolia, fees are required, so you need to fund the contract with LINK tokens. Open MetaMask and send 1 testnet LINK on Arbitrum Sepolia to the verifier contract address you saved earlier.

Verify a report onchain

  1. In Remix, on the Deploy & Run Transactions tab, expand your verifier contract under the Deployed Contracts section.

  2. Fill in the verifyReport function input parameter with the report payload you want to verify. You can use the following full report payload obtained in the Fetch and decode report via a REST API guide as an example:

    0x000660403d36be006d0c15d9b306f93c8660c5cfeab7db8e28c78ba316d395970000000000000000000000000000000000000000000000000000000032c3780a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000280010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200003c8e550d2fc5304993010112de9b69798297e4cc11990ee6250e464daf760000000000000000000000000000000000000000000000000000000006706e595000000000000000000000000000000000000000000000000000000006706e595000000000000000000000000000000000000000000000000000025bd3eb74c080000000000000000000000000000000000000000000000000021c6a95c654c7400000000000000000000000000000000000000000000000000000000670837150000000000000000000000000000000000000000000000079a2ab4077fc8fc6000000000000000000000000000000000000000000000000799fcb42536dfd8300000000000000000000000000000000000000000000000079a59496c3f29a0000000000000000000000000000000000000000000000000000000000000000002bd4acd37ce3cd5799de05d156ab328a5effd94468ebbaf2ff18d13d9631259cbe66cca01af6a8bb36e79d2d731a44e16791ee31e46ce27ed6530f1590cd7734c0000000000000000000000000000000000000000000000000000000000000002391562f1f2e4986bdb978fbf5ee27f7012992a79301af42d3473761ef2ede6271a61fbf4b32ac5be68a598bcfa523e035b624dab3b3d9a46276834f824ee592a
    
    Chainlink Data Streams Remix Deployed ClientReportsVerifier Contract
  3. Click the verifyReport button to call the function. MetaMask prompts you to accept the transaction.

  4. Click the lastDecodedPrice getter function to view the decoded price from the verified report. The answer on the ETH/USD stream uses 18 decimal places, so an answer of 3257579704051546000000 indicates an ETH/USD price of 3,257.579704051546. Each stream uses a different number of decimal places for answers. See the Stream Addresses page for more information.

    Chainlink Data Streams - Price from Verified Report

Examine the code

The example code you deployed has all the interfaces and functions required to verify Data Streams reports onchain.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {Common} from "@chainlink/contracts/src/v0.8/llo-feeds/libraries/Common.sol";
import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/v0.3.0/interfaces/IVerifierFeeManager.sol";
import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE FOR DEMONSTRATION PURPOSES.
 * DO NOT USE THIS CODE IN PRODUCTION.
 *
 *  This contract can verify Chainlink Data Streams reports onchain and pay
 *  the verification fee in LINK (when required).
 *
 * - If `VerifierProxy.s_feeManager()` returns a non-zero address, the network
 *   expects you to interact with that FeeManager for every verification call:
 *   quote fees, approve the RewardManager, then call `verify()`.
 *
 * - If `s_feeManager()` returns the zero address, no FeeManager contract has
 *   been deployed on that chain. In that case there is nothing to quote or pay
 *   onchain, so the contract skips the fee logic entirely.
 *
 *  The `if (address(feeManager) != address(0))` check below chooses the
 *  correct path automatically, making the same bytecode usable on any chain.
 */

// ────────────────────────────────────────────────────────────────────────────
//  Interfaces
// ────────────────────────────────────────────────────────────────────────────

interface IVerifierProxy {
    /**
     * @notice Route a report to the correct verifier and (optionally) bill fees.
     * @param payload           Full report payload (header + signed report).
     * @param parameterPayload  ABI-encoded fee metadata.
     */
    function verify(
        bytes calldata payload,
        bytes calldata parameterPayload
    ) external payable returns (bytes memory verifierResponse);

    function verifyBulk(
        bytes[] calldata payloads,
        bytes calldata parameterPayload
    ) external payable returns (bytes[] memory verifiedReports);

    function s_feeManager() external view returns (IVerifierFeeManager);
}

interface IFeeManager {
    /**
     * @return fee, reward, totalDiscount
     */
    function getFeeAndReward(
        address subscriber,
        bytes memory unverifiedReport,
        address quoteAddress
    ) external returns (Common.Asset memory, Common.Asset memory, uint256);

    function i_linkAddress() external view returns (address);

    function i_nativeAddress() external view returns (address);

    function i_rewardManager() external view returns (address);
}

// ────────────────────────────────────────────────────────────────────────────
//  Contract
// ────────────────────────────────────────────────────────────────────────────

/**
 * @dev This contract implements functionality to verify Data Streams reports from
 * the Data Streams API, with payment in LINK tokens.
 */
contract ClientReportsVerifier {
    // ----------------- Errors -----------------
    error NothingToWithdraw();
    error NotOwner(address caller);
    error InvalidReportVersion(uint16 version);

    // ----------------- Report schemas -----------------
    // More info: https://docs.chain.link/data-streams/reference/report-schema
    /**
     * @dev Data Streams report schema v3 (crypto streams).
     *      Prices, bids and asks use 8 or 18 decimals depending on the stream.
     */
    struct ReportV3 {
        bytes32 feedId;
        uint32 validFromTimestamp;
        uint32 observationsTimestamp;
        uint192 nativeFee;
        uint192 linkFee;
        uint32 expiresAt;
        int192 price;
        int192 bid;
        int192 ask;
    }

    /**
     * @dev Data Streams report schema v4 (RWA streams).
     */
    struct ReportV4 {
        bytes32 feedId;
        uint32 validFromTimestamp;
        uint32 observationsTimestamp;
        uint192 nativeFee;
        uint192 linkFee;
        uint32 expiresAt;
        int192 price;
        uint32 marketStatus;
    }

    // ----------------- Storage -----------------
    IVerifierProxy public immutable i_verifierProxy;
    address private immutable i_owner;

    int192 public lastDecodedPrice;

    // ----------------- Events -----------------
    event DecodedPrice(int192 price);

    // ----------------- Constructor / modifier -----------------
    /**
     * @param _verifierProxy Address of the VerifierProxy on the target network.
     *        Addresses: https://docs.chain.link/data-streams/crypto-streams
     */
    constructor(address _verifierProxy) {
        i_owner = msg.sender;
        i_verifierProxy = IVerifierProxy(_verifierProxy);
    }

    modifier onlyOwner() {
        if (msg.sender != i_owner) revert NotOwner(msg.sender);
        _;
    }

    // ----------------- Public API -----------------

    /**
     * @notice Verify a Data Streams report (schema v3 or v4).
     *
     * @dev Steps:
     *  1. Decode the unverified report to get `reportData`.
     *  2. Read the first two bytes → schema version (`0x0003` or `0x0004`).
     *     - Revert if the version is unsupported.
     *  3. Fee handling:
     *     - Query `s_feeManager()` on the proxy.
     *       – Non-zero → quote the fee, approve the RewardManager,
     *         ABI-encode the fee token address for `verify()`.
     *       – Zero     → no FeeManager; skip quoting/approval and pass `""`.
     *  4. Call `VerifierProxy.verify()`.
     *  5. Decode the verified report into the correct struct and emit the price.
     *
     *  @param unverifiedReport Full payload returned by Streams Direct.
     *  @custom:reverts InvalidReportVersion when schema ≠ v3/v4.
     */
    function verifyReport(bytes memory unverifiedReport) external {
        // ─── 1. & 2. Extract reportData and schema version ──
        (, bytes memory reportData) = abi.decode(
            unverifiedReport,
            (bytes32[3], bytes)
        );

        uint16 reportVersion = (uint16(uint8(reportData[0])) << 8) |
            uint16(uint8(reportData[1]));
        if (reportVersion != 3 && reportVersion != 4)
            revert InvalidReportVersion(reportVersion);

        // ─── 3. Fee handling ──
        IFeeManager feeManager = IFeeManager(
            address(i_verifierProxy.s_feeManager())
        );

        bytes memory parameterPayload;
        if (address(feeManager) != address(0)) {
            // FeeManager exists — always quote & approve
            address feeToken = feeManager.i_linkAddress();

            (Common.Asset memory fee, , ) = feeManager.getFeeAndReward(
                address(this),
                reportData,
                feeToken
            );

            IERC20(feeToken).approve(feeManager.i_rewardManager(), fee.amount);
            parameterPayload = abi.encode(feeToken);
        } else {
            // No FeeManager deployed on this chain
            parameterPayload = bytes("");
        }

        // ─── 4. Verify through the proxy ──
        bytes memory verified = i_verifierProxy.verify(
            unverifiedReport,
            parameterPayload
        );

        // ─── 5. Decode & store price ──
        if (reportVersion == 3) {
            int192 price = abi.decode(verified, (ReportV3)).price;
            lastDecodedPrice = price;
            emit DecodedPrice(price);
        } else {
            int192 price = abi.decode(verified, (ReportV4)).price;
            lastDecodedPrice = price;
            emit DecodedPrice(price);
        }
    }

    /**
     * @notice Withdraw all balance of an ERC-20 token held by this contract.
     * @param _beneficiary Address that receives the tokens.
     * @param _token       ERC-20 token address.
     */
    function withdrawToken(
        address _beneficiary,
        address _token
    ) external onlyOwner {
        uint256 amount = IERC20(_token).balanceOf(address(this));
        if (amount == 0) revert NothingToWithdraw();
        IERC20(_token).safeTransfer(_beneficiary, amount);
    }
}

Initializing the contract

When deploying the contract, you define the verifier proxy address for the stream you want to read from. You can find this address on the Stream Addresses page. The verifier proxy address provides functions that are required for this example:

  • The s_feeManager function to estimate the verification fees.
  • The verify function to verify the report onchain.

Verifying a report

The verifyReport function is the core function that handles onchain report verification. Here's how it works:

  • Report data extraction:

    • The function decodes the unverifiedReport to extract the report data.
    • It then extracts the report version by reading the first two bytes of the report data, which correspond to the schema version encoded in the stream ID:
    • If the report version is unsupported, the function reverts with an InvalidReportVersion error.
  • Fee calculation and handling:

    • The function first checks if a FeeManager contract exists by querying s_feeManager() on the verifier proxy.
    • If a FeeManager exists (non-zero address):
      • It calculates the fees required for verification using the getFeeAndReward function.
      • It approves the RewardManager contract to spend the calculated amount of LINK tokens from the contract's balance.
      • It encodes the fee token address into the parameterPayload for the verification call.
      • FeeManager contracts are currently deployed on: Arbitrum, Avalanche, Base, Blast, Bob, Ink, Linea, OP, Scroll, Soneium, and ZKSync.
    • If no FeeManager exists (zero address):
      • The function skips the fee calculation and approval steps entirely.
      • It passes an empty parameterPayload to the verification call.
    • This automatic detection makes the contract compatible with any network, regardless of whether fee management is deployed.
  • Report verification:

    • The verify function of the verifier proxy is called to perform the actual verification.
    • It passes the unverifiedReport and the parameterPayload (which contains either the encoded fee token address or empty bytes) as parameters.
  • Data decoding:

    • Depending on the report version, the function decodes the verified report data into the appropriate struct (ReportV3 or ReportV4).
    • It emits a DecodedPrice event with the price extracted from the verified report.
    • The lastDecodedPrice state variable is updated with the new price.

Additional functionality

The contract also includes:

  • Owner-only token withdrawal: The withdrawToken function allows the contract owner to withdraw any ERC-20 tokens (including LINK) from the contract.
  • Enhanced error handling: The contract includes specific error types (InvalidReportVersion, NotOwner, NothingToWithdraw) for better debugging and user experience.
  • Cross-chain compatibility: The automatic FeeManager detection makes the same contract code work on any supported network, whether fees are required or not.

What's next

Get the latest Chainlink content straight to your inbox.