# Verify Report Data Onchain (EVM)
Source: https://docs.chain.link/datalink/pull-delivery/tutorials/onchain-verification-evm

> For the complete documentation index, see [llms.txt](/llms.txt).

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 fetch reports via the REST API or WebSocket connection. Refer to the following guides for more information:

- [Fetch and decode reports (API) using the Go or Rust SDK](/datalink/pull-delivery/tutorials/fetch-decode/api-go)
- [Stream and decode reports (WebSocket) using the Go or Rust SDK](/datalink/pull-delivery/tutorials/stream-decode/ws-go)

## Requirements

- This guide requires testnet ETH and LINK on *Arbitrum Sepolia*. Both are available at [faucets.chain.link](https://faucets.chain.link/arbitrum-sepolia).
- Learn how to [Fund your contract with LINK](/resources/fund-your-contract).

## 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](https://remix.ethereum.org/#url=https://docs.chain.link/samples/DataLink/ClientReportsVerifier.sol) contract in Remix.

   [Open ClientReportsVerifier.sol in Remix](https://remix.ethereum.org/#url=https://docs.chain.link/samples/DataLink/ClientReportsVerifier.sol)

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

   (Image: Image)

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](/resources/link-token-contracts#arbitrum-sepolia-testnet) 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*.

   (Image: Image)

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 [Verifier Proxy Addresses](/datalink/pull-delivery/verifier-proxy-addresses) page.

   (Image: Image)

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.

   (Image: Image)

### 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](/datalink/pull-delivery/tutorials/fetch-decode/api-go) guide (EUR/USD feed) as an example:

   ```
   0x00090d9e8d96765a0c49e03a6ae05c82e8f8de70cf179baa632f18313e54bd69000000000000000000000000000000000000000000000000000000000041438a000000000000000000000000000000000000000000000000000000030000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000260000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000004b9905d8337c34e00f8dbe31619428bac5c3937e73e6af75c71780f1770ce00000000000000000000000000000000000000000000000000000000683f13dd00000000000000000000000000000000000000000000000000000000683f13dd00000000000000000000000000000000000000000000000000006e0e3915bcc3000000000000000000000000000000000000000000000000004edc1454fb6ef0000000000000000000000000000000000000000000000000000000006866a0dd0000000000000000000000000000000000000000000000000fcaa20569eac064000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000027b160a6824ccce49dc0bd19f636c40de2f3033410c7d1a7400b9a3cb0073d19dde0f87cfd6d9ce03156464a49cacb07136d2e7d717efcf42bc2795fd5c513e4a00000000000000000000000000000000000000000000000000000000000000025e075a9d8a6223ce2b9e524a7b5a563c2924a67b544e6676a751f5374b2a42ee37684b560eb72546f87b7287cefc668705461b7f4ebe4dabd7babe397cc98b89
   ```

   {" "}

   (Image: Image)

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 EUR/USD stream uses 18 decimal places, so an answer of `1137900000000000100` indicates an EUR/USD price of 1.1379000000000001. **Note**: Each feed may use a different number of decimal places for answers.

   (Image: Image)

## Examine the code

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

```sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

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 "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/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 DataLink 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 DataLink reports from
 * the API, with payment in LINK tokens.
 */
contract ClientReportsVerifier {
  // ----------------- Errors -----------------
  error NothingToWithdraw();
  error NotOwner(address caller);
  error InvalidReportVersion(uint16 version);

  // ----------------- Report schemas -----------------
  // Extract schema version from feed ID (first 2 bytes of the feed ID)
  /**
   * @dev DataLink report schema v3.
   *      Prices, bids and asks use 8 or 18 decimals depending on the feed.
   */
  struct ReportV3 {
    bytes32 feedId;
    uint32 validFromTimestamp;
    uint32 observationsTimestamp;
    uint192 nativeFee;
    uint192 linkFee;
    uint32 expiresAt;
    int192 price;
    int192 bid;
    int192 ask;
  }

  /**
   * @dev DataLink report schema v4.
   */
  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/datalink/pull-delivery/verifier-proxy-addresses
   */
  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 DataLink 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.
   *  @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 feed you want to read from. You can find this address on the [Verifier Proxy Addresses](/datalink/pull-delivery/verifier-proxy-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 feed 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.