Verify report data onchain

In this tutorial, 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 verifier proxy address corresponding to the Data Streams feed you want to read from. You can find this address on the Data Streams Feed IDs page. The verifier proxy address for the ETH/USD feed on Arbitrum Sepolia is 0x2ff010DEbC1297f19579B4246cad07bd24F2488A.

    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 later.

    Chainlink Data Streams Remix Deployed ClientReportsVerifier Contract

Fund the verifier contract

In this example, the verifier contract pays for onchain verification of reports in 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:

    0x0006f9b553e393ced311551efd30d1decedb63d76ad41737462e2cdbbdff15780000000000000000000000000000000000000000000000000000000020a8f908000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000028001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000120000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782000000000000000000000000000000000000000000000000000000006628c4ad000000000000000000000000000000000000000000000000000000006628c4ad00000000000000000000000000000000000000000000000000001beb59c3322c0000000000000000000000000000000000000000000000000016fad1097644cc00000000000000000000000000000000000000000000000000000000662a162d0000000000000000000000000000000000000000000000b097fff9701a850a800000000000000000000000000000000000000000000000b097892cdef361d7000000000000000000000000000000000000000000000000b098be558e09ed02c0000000000000000000000000000000000000000000000000000000000000000262810ea0ddf1d0883c6bab3cc10215ad7babd96fee6abd62b66f1cf8d8ef88c12dbae561312990e0a03945df9baf01d599354232d422772bb4fecc563baa96a500000000000000000000000000000000000000000000000000000000000000023a8e6710120b441f06d7475c2e207867f53cb4d0fb7387880109b3a2192b1b4027cce218afeeb5b2b2110a9bfac8e4a976f5e2c5e11e08afceafda9a8e13aa99
    
    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 last_decoded_price getter function to view the decoded price from the verified report. The answer on the ETH/USD feed uses 18 decimal places, so an answer of 3257579704051546000000 indicates an ETH/USD price of 3,257.579704051546. Each Data Streams feed uses a different number of decimal places for answers. See the Data Streams Feed IDs 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 {IRewardManager} from "@chainlink/contracts/src/v0.8/llo-feeds/interfaces/IRewardManager.sol";
import {IVerifierFeeManager} from "@chainlink/contracts/src/v0.8/llo-feeds/interfaces/IVerifierFeeManager.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/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.
 */

// Custom interfaces for IVerifierProxy and IFeeManager
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.
     * @param parameterPayload Fee metadata for billing. In the current implementation,
     * this consists of the abi-encoded address of the ERC-20 token used for fees.
     * @return verifierResponse The encoded report from the verifier.
     */
    function verify(
        bytes calldata payload,
        bytes calldata parameterPayload
    ) external payable returns (bytes memory verifierResponse);

    /**
     * @notice Verifies multiple reports in bulk, ensuring that each is signed correctly,
     * routes them to the appropriate verifier, and handles billing for the verification process.
     * @param payloads An array of encoded data to be verified, where each entry includes
     * the signed report.
     * @param parameterPayload Fee metadata for billing. In the current implementation,
     * this consists of the abi-encoded address of the ERC-20 token used for fees.
     * @return verifiedReports An array of encoded reports returned from the verifier.
     */
    function verifyBulk(
        bytes[] calldata payloads,
        bytes calldata parameterPayload
    ) external payable returns (bytes[] memory verifiedReports);

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

interface IFeeManager {
    /**
     * @notice Calculates the fee and reward associated with verifying a report, including discounts for subscribers.
     * This function assesses the fee and reward for report verification, applying a discount for recognized subscriber addresses.
     * @param subscriber The address attempting to verify the report. A discount is applied if this address
     * is recognized as a subscriber.
     * @param unverifiedReport The report data awaiting verification. The content of this report is used to
     * determine the base fee and reward, before considering subscriber discounts.
     * @param quoteAddress The payment token address used for quoting fees and rewards.
     * @return fee The fee assessed for verifying the report, with subscriber discounts applied where applicable.
     * @return reward The reward allocated to the caller for successfully verifying the report.
     * @return totalDiscount The total discount amount deducted from the fee for subscribers.
     */
    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);
}

/**
 * @dev This contract implements functionality to verify Data Streams reports from
 * the Streams Direct API or WebSocket connection, with payment in LINK tokens.
 */
contract ClientReportsVerifier {
    error NothingToWithdraw(); // Thrown when a withdrawal attempt is made but the contract holds no tokens of the specified type.
    error NotOwner(address caller); // Thrown when a caller tries to execute a function that is restricted to the contract's owner.

    /**
     * @dev Represents a data report from a Data Streams feed.
     * The `price`, `bid`, and `ask` values are carried to either 8 or 18 decimal places, depending on the feed.
     * For more information, see https://docs.chain.link/data-streams/stream-ids.
     */
    struct Report {
        bytes32 feedId; // The feed ID the report has data for
        uint32 validFromTimestamp; // Earliest timestamp for which price is applicable
        uint32 observationsTimestamp; // Latest timestamp for which price is applicable
        uint192 nativeFee; // Base cost to validate a transaction using the report, denominated in the chain’s native token (WETH/ETH)
        uint192 linkFee; // Base cost to validate a transaction using the report, denominated in LINK
        uint32 expiresAt; // Latest timestamp where the report can be verified onchain
        int192 price; // DON consensus median price (8 or 18 decimals)
        int192 bid; // Simulated price impact of a buy order up to the X% depth of liquidity utilisation (8 or 18 decimals)
        int192 ask; // Simulated price impact of a sell order up to the X% depth of liquidity utilisation (8 or 18 decimals)
    }

    IVerifierProxy public s_verifierProxy;

    address private s_owner;
    int192 public last_decoded_price;

    event DecodedPrice(int192);

    /**
     * @param _verifierProxy The address of the VerifierProxy contract.
     * You can find these addresses on https://docs.chain.link/data-streams/stream-ids
     */
    constructor(address _verifierProxy) {
        s_owner = msg.sender;
        s_verifierProxy = IVerifierProxy(_verifierProxy);
    }

    /// @notice Checks if the caller is the owner of the contract.
    modifier onlyOwner() {
        if (msg.sender != s_owner) revert NotOwner(msg.sender);
        _;
    }

    /**
     * @notice Verifies a report and handles fee payment.
     * @dev Decodes the unverified report, calculates fees, approves token spending, and verifies the report.
     * Emits a DecodedPrice event upon successful verification and stores the price from the report in `last_decoded_price`.
     * @param unverifiedReport The encoded report data to be verified.
     */
    function verifyReport(bytes memory unverifiedReport) external {
        // Report verification fees
        IFeeManager feeManager = IFeeManager(
            address(s_verifierProxy.s_feeManager())
        );

        IRewardManager rewardManager = IRewardManager(
            address(feeManager.i_rewardManager())
        );

        (, /* bytes32[3] reportContextData */ bytes memory reportData) = abi
            .decode(unverifiedReport, (bytes32[3], bytes));

        address feeTokenAddress = feeManager.i_linkAddress();

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

        // Approve rewardManager to spend this contract's balance in fees
        IERC20(feeTokenAddress).approve(address(rewardManager), fee.amount);

        // Verify the report
        bytes memory verifiedReportData = s_verifierProxy.verify(
            unverifiedReport,
            abi.encode(feeTokenAddress)
        );

        // Decode verified report data into a Report struct
        // If your report is a PremiumReport, you should decode it as a PremiumReport
        Report memory verifiedReport = abi.decode(verifiedReportData, (Report));

        // Log price from report
        emit DecodedPrice(verifiedReport.price);

        // Store the price from the report
        last_decoded_price = verifiedReport.price;
    }

    /**
     * @notice Withdraws all tokens of a specific ERC20 token type to a beneficiary address.
     * @dev Utilizes SafeERC20's safeTransfer for secure token transfer. Reverts if the contract's balance of the specified token is zero.
     * @param _beneficiary Address to which the tokens will be sent. Must not be the zero address.
     * @param _token Address of the ERC20 token to be withdrawn. Must be a valid ERC20 token contract.
     */
    function withdrawToken(
        address _beneficiary,
        address _token // LINK token address on Arbitrum Sepolia: 0x779877A7B0D9E8603169DdbD7836e478b4624789
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));

        // Revert if there is nothing to withdraw
        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 Data Streams feed you want to read from. You can find this address on the Data Streams Feed IDs 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 main function of the contract that verifies the report onchain. It follows the steps below:

  • Fee calculation: It interacts with the FeeManager contract, accessible via the verifier proxy contract, to determine the fees associated with verifying the report.

  • Token approval: It grants approval to the rewardManager contract to spend the required amount of LINK tokens from its balance.

  • Report verification: The core verification step involves calling the verify function from the verifier proxy contract. This function takes the (unverified) report payload and the encoded fee token address as inputs and returns the verified report data.

  • Data decoding and storage: In this example, the verified report data is decoded into a Report struct, extracting the report data. The extracted price data is then emitted through the DecodedPrice event and stored in the last_decoded_price state variable.

What's next

Get the latest Chainlink content straight to your inbox.