Skip to main content
Version: Endpoint V2 Docs

Omnichain Queries (LayerZero Read)

LayerZero Read (lzRead) enables smart contracts to request and retrieve on-chain state from other blockchains using LayerZero's cross-chain infrastructure. Unlike traditional messaging that sends data from source to destination, lzRead implements a request-response pattern where contracts can pull external state data from other blockchains.

For conceptual information about how Omnichain Queries work, see the Read Standard Overview.

Key Differences from Messaging

FeatureOmnichain MessageOmnichain Read
FlowSource sends data to destinationSource requests data, source receives response
Databytes sent = bytes receivedbytes request ≠ bytes response
PurposePush state changes to other chainsPull external state from other chains

Supported Chains

lzRead requires compatible Message Libraries (ReadLib1002) and DVNs with archival node access. See Read Paths for available chains and DVNs.

Read DVNs Light Read DVNs Dark

Installation

To start using LayerZero Read in a new project, use the LayerZero CLI tool, create-lz-oapp. The CLI tool allows developers to create any omnichain application with read capabilities quickly! Get started by running the following from your command line:

LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example oapp-read

Select the OApp Read template when prompted. This creates a complete project with:

  • Example contracts with read capabilities
  • Cross-chain unit tests for read operations
  • Custom LayerZero read configuration files
  • Deployment scripts and setup

To use LayerZero Read contracts in an existing project, you can install the OApp package directly:

npm install @layerzerolabs/oapp-evm
info

LayerZero contracts work with both OpenZeppelin V5 and V4 contracts. Specify your desired version in your project's package.json:

"resolutions": {
"@openzeppelin/contracts": "^5.0.1",
}

Custom Read Contract

To build your own cross-chain read application, inherit from OAppRead.sol and implement three key pieces:

  1. Read request construction: How you build queries for external data
  2. Fee estimation: How you calculate costs before sending requests
  3. Response handling: How you process returned data in _lzReceive

Below is the complete example that comes with the CLI tool, showing:

  • A constructor setting up the LayerZero endpoint, owner, and read channel
  • A readData(...) function that builds and sends read requests
  • A quoteReadFee(...) function to estimate costs before sending
  • An override of _lzReceive(...) that processes returned data
  • A target contract interface for type-safe interactions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Import necessary interfaces and contracts
import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol";
import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { ReadCodecV1, EVMCallRequestV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol";
import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/// @title IExampleContract
/// @notice Interface for the ExampleContract's `data()` function.
interface IExampleContract {
function data() external view returns (uint256);
}

/// @title ReadPublic
/// @notice An OAppRead contract example to read a public state variable from another chain.
contract ReadPublic is OAppRead, OAppOptionsType3 {
/// @notice Emitted when the data is received.
/// @param data The value of the public state variable.
event DataReceived(uint256 data);

/// @notice LayerZero read channel ID.
uint32 public READ_CHANNEL;

/// @notice Message type for the read operation.
uint16 public constant READ_TYPE = 1;

/**
* @notice Constructor to initialize the OAppRead contract.
*
* @param _endpoint The LayerZero endpoint contract address.
* @param _delegate The address that will have ownership privileges.
* @param _readChannel The LayerZero read channel ID.
*/
constructor(
address _endpoint,
address _delegate,
uint32 _readChannel
) OAppRead(_endpoint, _delegate) Ownable(_delegate) {
READ_CHANNEL = _readChannel;
_setPeer(_readChannel, AddressCast.toBytes32(address(this)));
}

// ──────────────────────────────────────────────────────────────────────────────
// 0. (Optional) Quote business logic
//
// Example: Get a quote from the Endpoint for a cost estimate of reading data.
// Replace this to mirror your own read business logic.
// ──────────────────────────────────────────────────────────────────────────────

/**
* @notice Estimates the messaging fee required to perform the read operation.
*
* @param _targetContractAddress The address of the contract on the target chain containing the `data` variable.
* @param _targetEid The target chain's Endpoint ID.
* @param _extraOptions Additional messaging options.
*
* @return fee The estimated messaging fee.
*/
function quoteReadFee(
address _targetContractAddress,
uint32 _targetEid,
bytes calldata _extraOptions
) external view returns (MessagingFee memory fee) {
return
_quote(
READ_CHANNEL,
_getCmd(_targetContractAddress, _targetEid),
combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions),
false
);
}

// ──────────────────────────────────────────────────────────────────────────────
// 1a. Send business logic
//
// Example: send a read request to fetch data from a remote contract.
// Replace this with your own read request logic.
// ──────────────────────────────────────────────────────────────────────────────

/**
* @notice Sends a read request to fetch the public state variable `data`.
*
* @dev The caller must send enough ETH to cover the messaging fee.
*
* @param _targetContractAddress The address of the contract on the target chain containing the `data` variable.
* @param _targetEid The target chain's Endpoint ID.
* @param _extraOptions Additional messaging options.
*
* @return receipt The LayerZero messaging receipt for the request.
*/
function readData(
address _targetContractAddress,
uint32 _targetEid,
bytes calldata _extraOptions
) external payable returns (MessagingReceipt memory receipt) {
// 1. Build the read command for the target contract and function
bytes memory cmd = _getCmd(_targetContractAddress, _targetEid);

// 2. Send the read request via LayerZero
// - READ_CHANNEL: Special channel ID for read operations
// - cmd: Encoded read command with target details
// - combineOptions: Merge enforced options with caller-provided options
// - MessagingFee(msg.value, 0): Pay all fees in native gas; no ZRO
// - payable(msg.sender): Refund excess gas to caller
return
_lzSend(
READ_CHANNEL,
cmd,
combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions),
MessagingFee(msg.value, 0),
payable(msg.sender)
);
}

// ──────────────────────────────────────────────────────────────────────────────
// 1b. Read command construction
//
// This function defines WHAT data to fetch from the target network and WHERE to fetch it from.
// This is the core of LayerZero Read - specifying exactly which contract function to call
// on which chain and how to handle the request.
// ──────────────────────────────────────────────────────────────────────────────

/**
* @notice Constructs the read command to fetch the `data` variable from target chain.
* @dev This function defines the core read operation - what data to fetch and from where.
* Replace this logic to read different functions or data from your target contracts.
*
* @param _targetContractAddress The address of the contract containing the `data` variable.
* @param _targetEid The target chain's Endpoint ID.
*
* @return cmd The encoded command that specifies what data to read.
*/
function _getCmd(address _targetContractAddress, uint32 _targetEid) internal view returns (bytes memory cmd) {
// 1. Define WHAT function to call on the target contract
// Using the interface selector ensures type safety and correctness
// You can replace this with any public/external function or state variable
bytes memory callData = abi.encodeWithSelector(IExampleContract.data.selector);

// 2. Build the read request specifying WHERE and HOW to fetch the data
EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1);
readRequests[0] = EVMCallRequestV1({
appRequestLabel: 1, // Label for tracking this specific request
targetEid: _targetEid, // WHICH chain to read from
isBlockNum: false, // Use timestamp (not block number)
blockNumOrTimestamp: uint64(block.timestamp), // WHEN to read the state (current time)
confirmations: 15, // HOW many confirmations to wait for
to: _targetContractAddress, // WHERE - the contract address to call
callData: callData // WHAT - the function call to execute
});

// 3. Encode the complete read command
// No compute logic needed for simple data reading
// The appLabel (0) can be used to identify different types of read operations
cmd = ReadCodecV1.encode(0, readRequests);
}

// ──────────────────────────────────────────────────────────────────────────────
// 2. Receive business logic
//
// Override _lzReceive to handle the returned data from the read request.
// The base OAppReceiver.lzReceive ensures:
// • Only the LayerZero Endpoint can call this method
// • The sender is a registered peer (peers[srcEid] == origin.sender)
// ──────────────────────────────────────────────────────────────────────────────

/**
* @notice Handles the received data from the target chain.
*
* @dev This function is called internally by the LayerZero protocol.
* @dev _origin Metadata (source chain, sender address, nonce)
* @dev _guid Global unique ID for tracking this response
* @param _message The data returned from the read request (uint256 in this case)
* @dev _executor Executor address that delivered the response
* @dev _extraData Additional data from the Executor (unused here)
*/
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
// 1. Decode the returned data from bytes to uint256
uint256 data = abi.decode(_message, (uint256));

// 2. Emit an event with the received data
emit DataReceived(data);

// 3. (Optional) Apply your custom logic here.
// e.g., store the data, trigger additional actions, etc.
}

// ──────────────────────────────────────────────────────────────────────────────
// 3. Admin functions
//
// Functions for managing the read channel configuration.
// ──────────────────────────────────────────────────────────────────────────────

/**
* @notice Sets the LayerZero read channel.
*
* @dev Only callable by the owner.
*
* @param _channelId The channel ID to set.
* @param _active Flag to activate or deactivate the channel.
*/
function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner {
_setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0));
READ_CHANNEL = _channelId;
}
}

Target Contract

The read contract interacts with a simple target contract deployed on other chains. This example demonstrates how you can call a public data variable on a destination network and get the current state from another network using LayerZero Read:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
* @title ExampleContract
* @notice A simple contract with a public state variable that can be read cross-chain.
* @dev This contract would be deployed on target chains (e.g., Ethereum, Polygon, Arbitrum)
* and the ReadPublic contract can fetch its `data` value from any other supported chain.
*/
contract ExampleContract {
/// @notice Public state variable that can be read from other chains
/// @dev The public keyword automatically generates a getter function data()
uint256 public data;

constructor(uint256 _data) {
data = _data;
}
}

Cross-Chain Reading Example:

  • Deploy ExampleContract on a target network with data = 100
  • Deploy ReadPublic on your source network
  • Call readData(targetContractAddress, targetEid, "0x") from source
  • The contract's configured DVNs will fetch the current value of data (100) from the target and emit DataReceived(100) on source

This enables real-time access to state from any supported blockchain without complex bridging or manual oracle updates.

Constructor

  • Pass the Endpoint V2 address, owner address, and read channel ID into the base contracts.
    • OAppRead(_endpoint, _delegate) binds your contract to LayerZero and sets the delegate
    • Ownable(_delegate) makes the delegate the only address that can change configurations
    • _setPeer(_readChannel, AddressCast.toBytes32(address(this))) establishes the read channel

readData(...)

  1. Build the read command

    • _getCmd() constructs the query specifying what data to fetch and from where
    • Uses IExampleContract.data.selector for type-safe function selection
  2. Send the read request

    • _lzSend() packages and dispatches the read request via LayerZero
    • READ_CHANNEL is the special channel ID for read operations
    • _combineOptions() merges enforced options with caller-provided options

_lzReceive(...)

  1. Endpoint verification

    • Only the LayerZero Endpoint can invoke this function
    • The call succeeds only if the sender matches the registered read channel peer
  2. Decode the returned data

    • Use abi.decode(_message, (uint256)) to extract the original data
    • The data format matches what the target contract's data() function returns
  3. Process the result

    • Emit DataReceived event with the fetched data
    • Add any custom business logic needed for your application

(Optional) quoteReadFee(...)

You can call the internal _quote(...) method to get accurate cost estimates before sending read requests.

Example usage:

// Get fee estimate first
MessagingFee memory fee = readPublic.quoteReadFee(
targetContractAddress,
targetEid,
"0x" // no additional options
);

// Then send with the estimated fee
readPublic.readData{value: fee.nativeFee}(
targetContractAddress,
targetEid,
"0x"
);

Deployment and Wiring

lzRead wiring is significantly simpler than traditional cross-chain messaging setup. Unlike OApp messaging where you need to configure peer connections between each contract on every chain pathway, lzRead only requires configuring the source chain (where your OAppRead child contract lives and where response data will be returned to).

Key Simplifications:

  • Single-sided peer wiring: You only need to set the OAppRead address itself as the peer
  • Dynamic target selection: Target chains are specified in the read command itself, not in wiring
  • DVN requirements: DVNs must support the target chains and block.number or block.timestamp you want to read
  • Single-direction setup: Only configure the source chain to receive responses

This means you can read from any supported target chain without additional wiring by just specifying the target in your EVMCallRequestV1 and ensuring your DVNs support that chain.

Execution Options for lzRead

lzRead uses different execution options than standard messaging. Instead of addExecutorLzReceiveOption, you must use addExecutorLzReadOption with calldata size estimation:

// Standard messaging options
OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0);

// lzRead options (note the size parameter)
OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 64, 0);
// gas size value

Key difference: The size parameter estimates your response data size in bytes. If your actual response exceeds this size, the executor won't deliver automatically.

tip

Size estimation: uint256 = 32 bytes, address = 20 bytes, etc. The enforcedOptions in your configuration (shown below) should account for your expected response sizes. For details on options configuration, see Execution Options.

Deploy and Wire

Deploy your lzRead OApp:

# Deploy contracts
npx hardhat lz:deploy

Then, review the layerzero.config.ts with read-specific settings:

import {ChannelId, EndpointId} from '@layerzerolabs/lz-definitions';
import {ExecutorOptionType} from '@layerzerolabs/lz-v2-utilities';
import {type OAppReadOmniGraphHardhat, type OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat';

const arbsepContract: OmniPointHardhat = {
eid: EndpointId.ARBSEP_V2_TESTNET,
contractName: 'ReadPublic',
};

const config: OAppReadOmniGraphHardhat = {
contracts: [
{
contract: arbsepContract,
config: {
readChannelConfigs: [
{
channelId: ChannelId.READ_CHANNEL_1,
active: true,
readLibrary: '0x54320b901FDe49Ba98de821Ccf374BA4358a8bf6',
ulnConfig: {
requiredDVNs: ['0x5c8c267174e1f345234ff5315d6cfd6716763bac'],
executor: '0x5Df3a1cEbBD9c8BA7F8dF51Fd632A9aef8308897',
},
enforcedOptions: [
{
msgType: 1,
optionType: ExecutorOptionType.LZ_READ,
gas: 80000,
size: 1000000,
value: 0,
},
],
},
],
},
},
],
connections: [],
};

export default config;

Deploy and configure your read OApp:

# Wire read configuration
npx hardhat lz:oapp-read:wire --oapp-config layerzero.config.ts

This automatically:

  • Sets the ReadLib1002 as send/receive library
  • Configures required DVNs for read operations
  • Activates specified read channels
  • Sets up executor configuration

Usage

Once deployed and wired, you can begin reading data from contracts on other chains.

Read data

The LayerZero CLI provides a convenient task for reading cross-chain data that automatically handles fee estimation and transaction execution.

Using the Read Task

The CLI includes a built-in lz:oapp-read:read task that:

  1. Finds your deployed ReadPublic contract automatically
  2. Quotes the gas cost using your contract's quoteReadFee() function
  3. Sends the read request with the correct fee
  4. Provides tracking links for the transaction

Basic usage:

npx hardhat lz:oapp-read:read --target-contract 0x1234567890123456789012345678901234567890 --target-eid 30101

Required Parameters:

  • --target-contract: Address of the contract to read from on the target chain
  • --target-eid: Target chain endpoint ID (e.g., 30101 for Ethereum)

Optional Parameters:

  • --options: Additional execution options as hex string (default: "0x")

Example with options:

npx hardhat lz:oapp-read:read \
--target-contract 0x1234567890123456789012345678901234567890 \
--target-eid 30101 \
--options 0x00030100110100000000000000000000000000030d40

The task automatically:

  • Finds your deployed ReadPublic contract from deployment artifacts
  • Quotes the exact gas fee needed using quoteReadFee()
  • Sends the read request with proper fee payment
  • Provides block explorer and LayerZero Scan links for tracking
  • Shows the transaction details and gas usage

Example output:

✅ SENT_READ_REQUEST: Successfully sent read request from arbitrum-sepolia to ethereum
✅ TX_HASH: Block explorer link for source chain arbitrum-sepolia: https://sepolia.arbiscan.io/tx/0x...
✅ EXPLORER_LINK: LayerZero Scan link for tracking read request: https://testnet.layerzeroscan.com/tx/0x...

📖 Read request sent! The data will be received and emitted in a DataReceived event.
Check the ReadPublic contract for the DataReceived event to see the result.

Advanced Read Contracts

lzRead supports several advanced patterns for cross-chain data access. Each pattern addresses different use cases, from simple data retrieval to complex multi-chain aggregation with compute logic.

tip

Complete examples are available in the LayerZero devtools repository. These examples provide full contract implementations you can deploy and test.

Call View/Pure Functions

You can use lzRead to call any view or pure function on a target chain and bring the returned data back to your source chain contract. This is the fundamental lzRead pattern that enables cross-chain function execution without state changes.

Core concept: Instead of deploying identical contracts on every chain or building complex bridging infrastructure, lzRead lets you call functions on any supported chain and receive the results natively. The target function executes via eth_call, ensuring no state modification occurs.

Use cases:

  • Cross-chain calculations: Call mathematical functions, pricing algorithms, or complex computations
  • Remote contract queries: Access getter functions, view state, or computed values from contracts on other chains
  • Protocol integration: Query external protocols (like AMMs, lending protocols, oracles) without deploying wrappers
  • Data aggregation: Collect information from various chains' contracts for unified processing
  • Validation: Verify conditions or states across multiple chains before executing local logic

Key implementation details:

  • Single EVMCallRequestV1 targeting specific function with parameters
  • Target function must be view or pure to ensure no state changes
  • Raw response data returned directly to _lzReceive - no compute processing needed
  • Function selector and parameter encoding handled via standard ABI encoding
  • Works with any function signature: simple getters, complex multi-parameter functions, struct returns

Installation

Get started quickly with a pre-built lzRead example for reading view or pure functions:

LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example view-pure-read

This creates a complete lzRead project with:

  • Example contracts for reading view/pure functions
  • Deploy and configuration scripts
  • Test suites demonstrating all patterns
  • Ready-to-use implementations you can customize

Contract Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Import necessary interfaces and contracts
import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol";
import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";
import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { EVMCallRequestV1, ReadCodecV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol";

/// @title IExampleContract
/// @notice Interface for the ExampleContract's `add` function.
interface IExampleContract {
function add(uint256 a, uint256 b) external pure returns (uint256);
}

/// @title ReadViewOrPure Example
/// @notice An OAppRead contract that calls view/pure functions on target chains and receives results
contract ReadViewOrPure is OAppRead, OAppOptionsType3 {

/// @notice Emitted when cross-chain function data is successfully received
event SumReceived(uint256 sum);

/// @notice LayerZero read channel ID for cross-chain data requests
uint32 public READ_CHANNEL;

/// @notice Message type identifier for read operations
uint16 public constant READ_TYPE = 1;

/// @notice Target chain's LayerZero Endpoint ID (immutable after deployment)
uint32 public immutable targetEid;

/// @notice Address of the contract to read from on the target chain
address public immutable targetContractAddress;

/**
* @notice Initialize the cross-chain read contract
* @dev Sets up LayerZero connectivity and establishes read channel peer relationship
* @param _endpoint LayerZero endpoint address on the source chain
* @param _readChannel Read channel ID for this contract's operations
* @param _targetEid Destination chain's endpoint ID where target contract lives
* @param _targetContractAddress Contract address to read from on target chain
*/
constructor(
address _endpoint,
uint32 _readChannel,
uint32 _targetEid,
address _targetContractAddress
) OAppRead(_endpoint, msg.sender) Ownable(msg.sender) {
READ_CHANNEL = _readChannel;
targetEid = _targetEid;
targetContractAddress = _targetContractAddress;

// Establish read channel peer - contract reads from itself via LayerZero
_setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this)));
}

/**
* @notice Configure the LayerZero read channel for this contract
* @dev Owner-only function to activate/deactivate read channels
* @param _channelId Read channel ID to configure
* @param _active Whether to activate (true) or deactivate (false) the channel
*/
function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner {
// Set or clear the peer relationship for the read channel
_setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0));
READ_CHANNEL = _channelId;
}

/**
* @notice Execute a cross-chain read request to call the target function
* @dev Builds the read command and sends it via LayerZero messaging
* @param _a First parameter for the target function
* @param _b Second parameter for the target function
* @param _extraOptions Additional execution options (gas, value, etc.)
* @return receipt LayerZero messaging receipt containing transaction details
*/
function readSum(
uint256 _a,
uint256 _b,
bytes calldata _extraOptions
) external payable returns (MessagingReceipt memory) {
// 1. Build the read command specifying target function and parameters
bytes memory cmd = _getCmd(_a, _b);

// 2. Send the read request via LayerZero
return _lzSend(
READ_CHANNEL,
cmd,
combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions),
MessagingFee(msg.value, 0),
payable(msg.sender)
);
}

/**
* @notice Get estimated messaging fee for a cross-chain read operation
* @dev Calculates LayerZero fees before sending to avoid transaction failures
* @param _a First parameter for the target function
* @param _b Second parameter for the target function
* @param _extraOptions Additional execution options
* @return fee Estimated LayerZero messaging fee structure
*/
function quoteReadFee(
uint256 _a,
uint256 _b,
bytes calldata _extraOptions
) external view returns (MessagingFee memory fee) {
// Build the same command as readSum and quote its cost
return _quote(READ_CHANNEL, _getCmd(_a, _b), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false);
}

/**
* @notice Build the LayerZero read command for target function execution
* @dev Constructs EVMCallRequestV1 specifying what data to fetch and from where
* @param _a First parameter to pass to target function
* @param _b Second parameter to pass to target function
* @return Encoded read command for LayerZero execution
*/
function _getCmd(uint256 _a, uint256 _b) internal view returns (bytes memory) {
// 1. Build the function call data
// Encode the target function selector with parameters
bytes memory callData = abi.encodeWithSelector(IExampleContract.add.selector, _a, _b);

// 2. Create the read request structure
EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1);
readRequests[0] = EVMCallRequestV1({
appRequestLabel: 1, // Request identifier for tracking
targetEid: targetEid, // Which chain to read from
isBlockNum: false, // Use timestamp instead of block number for data freshness
blockNumOrTimestamp: uint64(block.timestamp), // Read current state
confirmations: 15, // Wait for block finality before executing
to: targetContractAddress, // Target contract address
callData: callData // The function call to execute
});

// 3. Encode the command (no compute logic needed for simple reads)
return ReadCodecV1.encode(0, readRequests);
}

/**
* @notice Process the received data from the target chain
* @dev Called by LayerZero when the read response is delivered
* @param _message Encoded response data from the target function call
*/
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
// 1. Validate response format
require(_message.length == 32, "Invalid message length");

// 2. Decode the returned data (matches target function return type)
uint256 sum = abi.decode(_message, (uint256));

// 3. Process the result (emit event, update state, trigger logic, etc.)
emit SumReceived(sum);
}
}

// Example target contract for demonstration
contract ExampleContract {
/**
* @notice Adds two numbers.
* @param a First number.
* @param b Second number.
* @return sum The sum of a and b.
*/
function add(uint256 a, uint256 b) external pure returns (uint256 sum) {
return a + b;
}
}

Cross-Chain View/Pure Function Reading:

  • Deploy ReadViewOrPure on your source network
  • Call readSum(5, 10, "0x") to execute the add function on the target chain
  • The contract's DVNs fetch the result directly and deliver it to SumReceived(15) event
  • No compute processing - raw response delivered directly to your contract

This enables direct access to any view/pure function across supported chains without complex bridging infrastructure.

Constructor

  • Pass the Endpoint V2 address, owner address, read channel ID, target chain ID, and target contract address
  • OAppRead(_endpoint, msg.sender) binds your contract to LayerZero and sets the delegate
  • Ownable(msg.sender) makes the deployer the only address that can change configurations
  • _setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this))) establishes the read channel peer relationship

readSum(...)

  1. Build the read command

    • _getCmd() constructs the query specifying target function with parameters
    • Uses IExampleContract.add.selector for type-safe function selection
  2. Send the read request

    • _lzSend() packages and dispatches the read request via LayerZero
    • combineOptions() merges enforced options with caller-provided options
    • Caller must provide sufficient native fee for cross-chain execution

_getCmd(...)

  1. Encode the function call

    • Build callData using function selector and parameters
    • Standard ABI encoding for target contract interface
  2. Create the read request

    • Single EVMCallRequestV1 targeting specific chain and contract
    • appRequestLabel: 1 for request tracking and identification
    • Uses current timestamp for fresh data reads
  3. Encode the command

    • ReadCodecV1.encode(0, readRequests) with no compute logic
    • AppLabel 0 indicates basic read without additional processing

_lzReceive(...)

  1. Endpoint verification

    • Only LayerZero Endpoint can invoke this function
    • Validates sender matches registered read channel peer
  2. Decode the response

    • Extract raw data using abi.decode(_message, (uint256))
    • Data format matches target function's return type exactly
  3. Process the result

    • Emit SumReceived event with the fetched data
    • Add custom business logic, state updates, or trigger additional operations

(Optional) quoteReadFee(...)

Estimates messaging fees before sending to avoid transaction failures:

// Get fee estimate first
MessagingFee memory fee = readContract.quoteReadFee(5, 10, "0x");

// Then send with estimated fee
readContract.readSum{value: fee.nativeFee}(5, 10, "0x");

Real-world applications

The example above shows a simple mathematical function, but lzRead can call any view/pure function across chains:

// Query token balances on other chains
function getBalance(address user) external view returns (uint256);

// Access oracle prices from different chains
function getLatestPrice() external view returns (uint256 price, uint256 timestamp);

// Check protocol states across deployments
function getTotalSupply() external view returns (uint256);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1);

// Validate conditions before cross-chain actions
function isEligibleForRewards(address user) external view returns (bool eligible, uint256 amount);

// Query governance states
function getProposalState(uint256 proposalId) external view returns (uint8 state);

The key advantage is data locality - instead of bridging tokens or deploying contracts everywhere, you can query any chain's data directly and use it in your source chain logic.

Add Compute Logic to Responses

Add off-chain data processing to transform, validate, or format response data before it reaches your contract. The compute layer executes between the DVN(s) getting the response data from your target contract and delivering it to your _lzReceive function, allowing complex data manipulation without additional gas costs.

Core concept: After DVNs fetch your requested data, the compute layer can process it off-chain using your custom lzMap and lzReduce functions. This enables data transformation, validation, aggregation, and formatting without consuming gas on your source chain.

How compute processing works:

  1. DVNs fetch data from your target contract using the specified function call
  2. lzMap executes (if configured) to transform each individual response
  3. lzReduce executes (if configured) to aggregate all mapped responses into a final result
  4. Final result delivered to your _lzReceive function on the source chain

The compute layer acts as a middleware processing step that runs off-chain but is cryptographically verified, giving you powerful data manipulation capabilities without gas costs.

Use cases:

  • Data transformation: Convert complex structs into simpler formats your contract needs
  • Response validation: Filter out invalid responses or apply business logic rules
  • Unit conversion: Convert between different decimal places, currencies, or measurement units
  • Data cleaning: Remove outliers, normalize formats, or standardize responses
  • Aggregation prep: Process individual responses before combining them
  • Format standardization: Ensure all responses follow consistent encoding patterns

Key implementation details:

  • Implement IOAppMapper for individual response processing and/or IOAppReducer for aggregation
  • Add EVMCallComputeV1 struct to specify compute configuration
  • Configure computeSetting: 0 = lzMap only, 1 = lzReduce only, 2 = both
  • Compute functions execute off-chain, reducing gas costs for complex operations
  • lzMap processes each response individually; lzReduce combines all mapped responses

Installation

Get started quickly with a pre-built lzRead compute example:

LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example view-pure-read

The generated project includes the ReadViewOrPureAndCompute contract demonstrating the complete compute pipeline.

Contract Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Import necessary interfaces and contracts
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol";
import { IOAppMapper } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppMapper.sol";
import { IOAppReducer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReducer.sol";
import { EVMCallRequestV1, EVMCallComputeV1, ReadCodecV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol";

import { AddressCast } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/AddressCast.sol";
import { MessagingFee, MessagingReceipt, ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";

/// @title IExampleContract
/// @notice Interface for the ExampleContract's `add` function.
interface IExampleContract {
function add(uint256 a, uint256 b) external pure returns (uint256);
}

/// @title ReadViewOrPureAndCompute
/// @notice Cross-chain read contract with compute processing for data transformation and aggregation
contract ReadViewOrPureAndCompute is OAppRead, IOAppMapper, IOAppReducer, OAppOptionsType3 {

/// @notice Emitted when final computed result is received from the compute pipeline
event SumReceived(uint256 sum);

/// @notice LayerZero read channel ID for cross-chain data requests with compute
uint32 public READ_CHANNEL;

/// @notice Message type identifier for read operations with compute processing
uint16 public constant READ_TYPE = 1;

/// @notice Target chain's LayerZero Endpoint ID (immutable after deployment)
uint32 public immutable targetEid;

/// @notice Address of the contract to read from on the target chain
address public immutable targetContractAddress;

/**
* @notice Initialize the cross-chain read contract with compute capabilities
* @dev Sets up LayerZero connectivity, establishes read channel, and enables compute processing
* @param _endpoint LayerZero endpoint address on the source chain
* @param _readChannel Read channel ID for this contract's operations
* @param _targetEid Destination chain's endpoint ID where target contract lives
* @param _targetContractAddress Contract address to read from on target chain
*/
constructor(
address _endpoint,
uint32 _readChannel,
uint32 _targetEid,
address _targetContractAddress
) OAppRead(_endpoint, msg.sender) Ownable(msg.sender) {
READ_CHANNEL = _readChannel;
targetEid = _targetEid;
targetContractAddress = _targetContractAddress;

// Establish read channel peer - contract processes its own compute functions
_setPeer(READ_CHANNEL, AddressCast.toBytes32(address(this)));
}

/**
* @notice Configure the LayerZero read channel for compute-enabled operations
* @dev Owner-only function to activate/deactivate read channels with compute processing
* @param _channelId Read channel ID to configure
* @param _active Whether to activate (true) or deactivate (false) the channel
*/
function setReadChannel(uint32 _channelId, bool _active) public override onlyOwner {
// Set or clear the peer relationship for compute-enabled read operations
_setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0));
READ_CHANNEL = _channelId;
}

/**
* @notice Execute a cross-chain read request with compute processing pipeline (Step 1)
* @dev Builds read command with compute configuration and sends via LayerZero
* @param _a First parameter for the target function
* @param _b Second parameter for the target function
* @param _extraOptions Additional execution options (gas, value, etc.)
* @return receipt LayerZero messaging receipt containing transaction details
*/
function readSum(
uint256 _a,
uint256 _b,
bytes calldata _extraOptions
) external payable returns (MessagingReceipt memory) {
// 1. Build the read command with compute configuration
bytes memory cmd = _getCmd(_a, _b);

// 2. Send the read request with compute processing enabled
return _lzSend(
READ_CHANNEL,
cmd,
combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions),
MessagingFee(msg.value, 0),
payable(msg.sender)
);
}

/**
* @notice Get estimated messaging fee for cross-chain read with compute processing
* @dev Calculates LayerZero fees including compute overhead before sending
* @param _a First parameter for the target function
* @param _b Second parameter for the target function
* @param _extraOptions Additional execution options
* @return fee Estimated LayerZero messaging fee structure
*/
function quoteReadFee(
uint256 _a,
uint256 _b,
bytes calldata _extraOptions
) external view returns (MessagingFee memory fee) {
// Build the same command as readSum (including compute config) and quote its cost
return _quote(READ_CHANNEL, _getCmd(_a, _b), combineOptions(READ_CHANNEL, READ_TYPE, _extraOptions), false);
}

/**
* @notice Build the LayerZero read command with compute processing configuration
* @dev Constructs both the target function call AND the compute pipeline setup
* @param _a First parameter to pass to target function
* @param _b Second parameter to pass to target function
* @return Encoded read command with compute configuration for LayerZero execution
*/
function _getCmd(uint256 _a, uint256 _b) internal view returns (bytes memory) {
// 1. Build the target function call data (same as basic read)
bytes memory callData = abi.encodeWithSelector(IExampleContract.add.selector, _a, _b);

// 2. Create the read request structure
EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](1);
readRequests[0] = EVMCallRequestV1({
appRequestLabel: 1, // Request identifier for tracking through compute pipeline
targetEid: targetEid, // Which chain to read from
isBlockNum: false, // Use timestamp for data freshness
blockNumOrTimestamp: uint64(block.timestamp), // Read current state
confirmations: 15, // Wait for block finality before processing
to: targetContractAddress, // Target contract address
callData: callData // The function call to execute
});
// 3. Configure the compute processing pipeline - THIS IS THE KEY DIFFERENCE
EVMCallComputeV1 memory computeRequest = EVMCallComputeV1({
computeSetting: 2, // 0=lzMap only, 1=lzReduce only, 2=both lzMap and lzReduce
targetEid: ILayerZeroEndpointV2(endpoint).eid(), // Execute compute on source chain (this chain)
isBlockNum: false, // Use timestamp for compute execution timing
blockNumOrTimestamp: uint64(block.timestamp), // When to execute compute functions
confirmations: 15, // Confirmations needed before compute processing begins
to: address(this) // Contract address containing lzMap/lzReduce implementations
});

// 4. Encode the complete command (read requests + compute configuration)
return ReadCodecV1.encode(0, readRequests, computeRequest);
}
/**
* @notice Transform individual read responses during compute processing (Step 2 of compute pipeline)
* @dev Called by LayerZero's compute layer for each raw response from target chains
* @param _request Original request data (unused in this example, but available for context)
* @param _response Raw response data from the target chain function call
* @return Processed response data to pass to lzReduce (or final result if no reduce step)
*/
function lzMap(
bytes calldata /*_request*/,
bytes calldata _response
) external pure override returns (bytes memory) {
// 1. Decode the raw response from target function (uint256 from add function)
uint256 sum = abi.decode(_response, (uint256));

// 2. Apply transformation logic (example: increment by 1)
// This could be: unit conversion, validation, filtering, formatting, etc.
sum += 1;

// 3. Re-encode for lzReduce or final delivery
return abi.encode(sum);
}

/**
* @notice Aggregate all mapped responses into final result (Step 3 of compute pipeline)
* @dev Called after all lzMap operations complete, receives array of mapped responses
* @param _cmd Original command data (unused in this example, but available for context)
* @param _responses Array of processed responses from lzMap function
* @return Final aggregated data to deliver to _lzReceive
*/
function lzReduce(
bytes calldata /*_cmd*/,
bytes[] calldata _responses
) external pure override returns (bytes memory) {
uint256 totalSum = 0;

// Process each mapped response and aggregate them
for (uint256 i = 0; i < _responses.length; i++) {
// 1. Validate each response format
require(_responses[i].length == 32, "Invalid response length");

// 2. Decode the mapped response
uint256 sum = abi.decode(_responses[i], (uint256));

// 3. Apply aggregation logic (example: sum all responses)
totalSum += sum;
}

// 4. Return final aggregated result
return abi.encode(totalSum);
}

/**
* @notice Process the final computed result from the compute pipeline (Step 4)
* @dev Called by LayerZero when compute processing is complete and result is delivered
* @param _message Final processed data from the compute pipeline (lzReduce output)
*/
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
// 1. Validate final result format
require(_message.length == 32, "Invalid message length");

// 2. Decode the final computed result
uint256 sum = abi.decode(_message, (uint256));

// 3. Process the result (emit event, update state, trigger logic, etc.)
emit SumReceived(sum);
}
}

Cross-Chain Reading with Compute Processing:

  • Deploy ReadViewOrPureAndCompute on your source network
  • Call readSum(5, 10, "0x") to execute the add function on the target chain with compute processing
  • The contract's DVNs fetch the result, lzMap transforms it (+1), lzReduce aggregates each response (by default only 1 response, so unchanged), and the final computed result is delivered to SumReceived(16) event

This enables sophisticated data processing pipelines where raw cross-chain data is transformed and aggregated off-chain before reaching your contract.

Constructor

  • Initialize the contract with compute capabilities enabled via IOAppMapper and IOAppReducer interfaces
  • Sets up LayerZero connectivity and establishes read channel peer relationship for compute operations
  • The contract becomes both the read requester and the compute processor (via address(this) in compute configuration)

readSum(...)

Step 1 of compute pipeline: Dispatch read request with compute command

  1. Build the compute command

    • _getCmd() constructs both the read request AND the compute configuration
    • Specifies which compute functions to use (lzMap, lzReduce, or both)
  2. Send the read request

    • _lzSend() packages and dispatches the read request with compute processing enabled
    • Higher fees due to compute overhead compared to basic reads

_getCmd(...)

Key difference from basic reads: Includes EVMCallComputeV1 configuration

  • Read request structure: Same as basic pattern - specifies target function and parameters
  • Compute configuration: Defines the processing pipeline that will execute after data retrieval
    • computeSetting: 2 enables both lzMap and lzReduce processing
    • to: address(this) specifies this contract contains the compute function implementations

lzMap(...)

Step 2 of compute pipeline: Individual response transformation

  1. Decode raw response
    • Extract data from target chain function call result
  2. Apply transformation logic
    • Convert formats, validate data, apply business rules
    • Example: increment by 1, but could be unit conversion, filtering, etc.
  3. Re-encode for next step
    • Prepare data for lzReduce or final delivery to _lzReceive

lzReduce(...)

Step 3 of compute pipeline: Response aggregation

  1. Process mapped responses
    • Receive array of all lzMap outputs
    • Validate each response format and content
  2. Apply aggregation logic
    • Combine responses using your business logic
    • Example: sum all values, but could be averaging, min/max, weighted calculations
  3. Return final result
    • Single aggregated value to deliver to _lzReceive

_lzReceive(...)

Step 4 of compute pipeline: Final result processing

  1. Receive computed result
    • Data has already been through lzMap and lzReduce processing
    • Final result is delivered, not raw target chain response
  2. Process final data
    • Emit events, update state, trigger additional logic
    • Result represents the fully processed and aggregated data

(Optional) quoteReadFee(...)

Fee estimation includes compute processing overhead. Costs are higher than basic reads due to:

  • Additional compute execution processing
  • Data transformation and aggregation operations
  • Multiple processing steps in the pipeline

Example usage:

// Get fee estimate for read with compute
MessagingFee memory fee = readContract.quoteReadFee(5, 10, "0x");

// Send with computed processing
readContract.readSum{value: fee.nativeFee}(5, 10, "0x");

Call Non-View Functions

lzRead can also query functions that aren't marked view or pure, but still return valuable data without modifying state. This pattern leverages eth_call to safely execute functions that would normally require gas, enabling access to sophisticated on-chain computations.

Core concept: Many useful functions (especially in DeFi) aren't marked view because they rely on calling other non-view functions internally, even though they don't modify state. lzRead uses eth_call to execute these functions safely, capturing their return values without gas costs or state changes.

Use cases:

  • DEX price quotations: Uniswap V3's quoteExactInputSingle simulates swaps to calculate output amounts
  • Lending protocol queries: Calculate borrow rates, collateral requirements, or liquidation thresholds
  • Yield farming calculations: Determine pending rewards, APR calculations, or harvest amounts
  • Options pricing: Complex mathematical models for derivative pricing
  • Arbitrage detection: Calculate profit opportunities across different protocols
  • Liquidation analysis: Determine if positions are liquidatable and expected returns

Why these functions aren't view:

  • They call other non-view functions internally (like Uniswap's swap simulation)
  • They use try-catch blocks or other constructs that prevent view designation
  • They access external contracts that may not be view-compatible
  • They perform complex state reads that the compiler can't verify as non-modifying

Key implementation details:

  • Functions must not revert during execution - test parameters thoroughly
  • Use proper struct encoding for complex parameters (like Uniswap's QuoteExactInputSingleParams)
  • Handle multi-return-value responses with correct ABI decoding
  • Target functions execute via eth_call, so no actual state changes or gas consumption occur
  • DVNs verify these calls can execute successfully before returning data

Installation

Get started quickly with a pre-built Uniswap V3 quote reader example:

LZ_ENABLE_READ_EXAMPLE=1 npx create-lz-oapp@latest --example uniswap-read

This creates a complete project with:

  • Uniswap V3 QuoterV2 integration contracts
  • Non-view function calling examples
  • Multi-chain price aggregation patterns
  • Ready-to-deploy implementations for major chains

Contract Example

// contracts/UniswapV3QuoteDemo.sol
//
// ──────────────────────────────────────────────────────────────────────────────
// 1b. Read Command Construction
// ──────────────────────────────────────────────────────────────────────────────
/// @notice Constructs the read command to fetch Uniswap V3 quotes from each configured chain.
/// @return cmd Encoded command for cross-chain price queries.
function getCmd() public view returns (bytes memory cmd) {
uint256 count = targetEids.length;
EVMCallRequestV1[] memory requests = new EVMCallRequestV1[](count);
for (uint256 i = 0; i < count; ++i) {
uint32 eid = targetEids[i];
ChainConfig memory cfg = chainConfigs[eid];
bytes memory data = abi.encodeWithSelector(
IQuoterV2.quoteExactInputSingle.selector,
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: cfg.tokenInAddress,
tokenOut: cfg.tokenOutAddress,
amountIn: 1 ether,
fee: cfg.fee,
sqrtPriceLimitX96: 0
})
);
requests[i] = EVMCallRequestV1({
appRequestLabel: uint16(i + 1),
targetEid: eid,
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp),
confirmations: cfg.confirmations,
to: cfg.quoterAddress,
callData: data
});
}
EVMCallComputeV1 memory compute = EVMCallComputeV1({
computeSetting: 2,
targetEid: ILayerZeroEndpointV2(endpoint).eid(),
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp),
confirmations: 15,
to: address(this)
});
return ReadCodecV1.encode(0, requests, compute);
}

// ──────────────────────────────────────────────────────────────────────────────
// 2. Map & Reduce Logic
// ──────────────────────────────────────────────────────────────────────────────

/// @notice Maps individual Uniswap quote responses to encoded amounts.
/// @param _response Raw response bytes from the quoted call.
/// @return Encoded amountOut for a single chain.
function lzMap(bytes calldata, bytes calldata _response) external pure returns (bytes memory) {
require(_response.length >= 32, "Invalid response length");
(uint256 amountOut,,,) = abi.decode(_response, (uint256, uint160, uint32, uint256));
return abi.encode(amountOut);
}

/// @notice Reduces multiple mapped responses to a single average value.
/// @param _responses Array of encoded amountOut responses from each chain.
/// @return Encoded average of all responses.
function lzReduce(bytes calldata, bytes[] calldata _responses) external pure returns (bytes memory) {
require(_responses.length > 0, "No responses");
uint256 sum;
for (uint i = 0; i < _responses.length; i++) {
sum += abi.decode(_responses[i], (uint256));
}
uint256 avg = sum / _responses.length;
return abi.encode(avg);
}

// ──────────────────────────────────────────────────────────────────────────────
// 3. Receive Business Logic
// ──────────────────────────────────────────────────────────────────────────────

/// @notice Handles the final averaged quote from LayerZero and emits the result.
/// @dev _origin LayerZero origin metadata (unused).
/// @dev _guid Unique message identifier (unused).
/// @param _message Encoded average price bytes.
function _lzReceive(
Origin calldata /*_origin*/, bytes32 /*_guid*/, bytes calldata _message,
address /*_executor*/, bytes calldata /*_extraData*/
) internal override {
uint256 averagePrice = abi.decode(_message, (uint256));
emit AggregatedPrice(averagePrice);
}

Cross-Chain Price Aggregation Example:

  • Deploy UniswapV3QuoteDemo on your source network (configured for Ethereum, Base, and Optimism)
  • Call readAverageUniswapPrice("0x") to query WETH/USDC prices across all three chains simultaneously
  • The contract's DVNs fetch prices from each chain's Uniswap V3 deployment
  • lzMap extracts the amountOut from each chain's complex response
  • lzReduce calculates the average price across all chains
  • Final averaged price is delivered to AggregatedPrice(averagePrice) event

This enables sophisticated cross-chain price feeds, governance aggregation, and multi-chain protocol monitoring in a single transaction.

Constructor

Pre-configures three major chains with their respective Uniswap V3 deployments using hardcoded constants:

  • Ethereum Mainnet: EID 30101 with WETH/USDC addresses and QuoterV2 contract
  • Base Mainnet: EID 30184 with chain-specific token addresses
  • Optimism Mainnet: EID 30111 with chain-specific token addresses
  • Sets up LayerZero connectivity and establishes read channel peer relationship
  • Key advantage: Ready-to-deploy with major chains pre-configured

readAverageUniswapPrice(...)

  1. Build multi-chain command

    • getCmd() constructs read requests for ALL three configured chains
    • Each request queries quoteExactInputSingle with 1 WETH input amount
  2. Send aggregated request

    • Single _lzSend() operation handles all three chains simultaneously
    • More cost-effective than separate requests per chain
    • Includes compute configuration for price averaging

getCmd(...)

Multi-chain request construction with compute:

  1. Iterate through target chains

    • Build EVMCallRequestV1 for Ethereum, Base, and Optimism
    • Use unique appRequestLabel (1, 2, 3) to track responses during compute processing
  2. Chain-specific parameters

    • Each request uses that chain's specific QuoterV2, WETH, and USDC addresses
    • Maintains consistent amountIn: 1 ether across all chains for comparable results
    • Uses chain-specific confirmation requirements (5 blocks each)
  3. Compute configuration

    • computeSetting: 2 enables both lzMap and lzReduce for response processing
    • targetEid points to source chain for compute execution
    • to: address(this) specifies this contract contains the compute functions

lzMap(...) - Price Extraction

Individual chain response processing:

// Uniswap returns: (amountOut, sqrtPriceX96After, initializedTicksCrossed, gasEstimate)
(uint256 amountOut,,,) = abi.decode(_response, (uint256, uint160, uint32, uint256));
return abi.encode(amountOut); // Extract only the price data we need

Purpose: Extract amountOut (USDC amount for 1 WETH) from Uniswap's complex 4-value response, normalizing all chains to simple price values.

lzReduce(...) - Price Averaging

Cross-chain aggregation logic:

uint256 sum;
for (uint i = 0; i < _responses.length; i++) {
sum += abi.decode(_responses[i], (uint256));
}
uint256 avg = sum / _responses.length; // Simple average across 3 chains

Current implementation: Simple arithmetic mean of all three chain prices.

Potential enhancements:

  • Weighted averaging: Weight by liquidity, volume, or chain importance
  • Outlier filtering: Remove prices that deviate significantly from median
  • Confidence scoring: Account for different chain finality requirements

_lzReceive(...) - Final Price Delivery

Receives the final aggregated price representing the cross-chain average WETH/USDC price:

  • Event emission: Emits AggregatedPrice(averagePrice) with the computed average
  • Result format: Single uint256 representing average USDC amount for 1 WETH across all three chains

Use cases for the aggregated price:

  • Cross-chain arbitrage detection: Compare with local prices to find opportunities
  • Multi-chain price oracles: Provide robust price feeds aggregating multiple sources
  • Risk management: Monitor price discrepancies across deployments
  • Liquidity routing: Direct users to chains with optimal pricing

Architecture Benefits

Single Transaction Efficiency:

  • One read request handles 3+ chains instead of separate transactions
  • Reduced gas costs and complexity compared to multiple individual requests

Atomic Consistency:

  • All chain data fetched and processed together
  • No timing discrepancies between separate async requests

Failure Resilience:

  • Built-in retry logic across all target chains
  • Graceful handling of individual chain failures without affecting others

Why These Functions Aren't View:

  • Internal non-view calls: Uniswap's quoter calls other non-view functions internally during swap simulation
  • Try-catch blocks: Error handling constructs prevent view designation even when no state changes occur
  • Compiler restrictions: Complex state reads that the compiler can't verify as non-modifying

DVN Verification: DVNs use eth_call to execute these functions, ensuring:

  • No actual state changes occur on the target chain
  • No gas consumption on the target chain
  • Results are cryptographically verified and delivered to your source chain

Multi-Chain Aggregation

Execute identical or related queries across multiple chains simultaneously and combine the results into a single, meaningful response. This is lzRead's most powerful pattern, enabling true cross-chain data synthesis and decision-making.

Core concept: Instead of making separate read requests to different chains and manually combining results, multi-chain aggregation fetches data from multiple networks in a single lzRead command. The compute layer processes and combines all responses before delivering the final result to your source chain.

Use cases:

  • Cross-chain price feeds: Get token prices from major DEXes on different chains and calculate weighted averages
  • Multi-chain governance: Aggregate voting results across different network deployments of your protocol
  • Liquidity analysis: Compare pool depths, trading volumes, and rates across chains to find optimal routing
  • Risk assessment: Analyze protocol health by checking reserves, utilization rates, and other metrics across deployments
  • Arbitrage detection: Find price discrepancies and calculate potential profits across multiple networks
  • Portfolio valuation: Calculate total holdings by querying balances and prices across user's multi-chain positions
  • Protocol synchronization: Monitor and compare state across different chain deployments

Architecture benefits:

  • Single transaction cost: One read request handles multiple chains instead of separate transactions
  • Atomic aggregation: All chain data is processed together, ensuring consistency
  • Reduced complexity: No need to manage multiple async requests or coordinate responses
  • Gas efficiency: Compute processing happens off-chain, minimizing source chain gas usage
  • Failure handling: Built-in retry and error handling across all target chains

Key implementation details:

  • Array of EVMCallRequestV1 structs, each targeting different chains/contracts
  • Unique appRequestLabel for each request to track responses during compute processing
  • lzMap processes each chain's response individually (normalization, validation)
  • lzReduce combines all mapped responses into final aggregated result
  • DVNs must support all target chains specified in your requests

Contract Example

Refer to the same UniswapV3QuoteDemo contract under Non-View Functions

Architecture Benefits

Single Transaction Efficiency:

  • One read request handles 3+ chains instead of separate transactions
  • Reduced gas costs and complexity compared to multiple individual requests

Atomic Consistency:

  • All chain data fetched and processed together
  • No timing discrepancies between separate async requests

Failure Resilience:

  • Built-in retry logic across all target chains
  • Graceful handling of individual chain failures without affecting others

Hybrid Messaging + Read

For applications that need both messaging and read capabilities:

contract HybridApp is OAppRead {
uint32 constant READ_CHANNEL_THRESHOLD = 4294965694;

function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) internal override {
if (_origin.srcEid > READ_CHANNEL_THRESHOLD) {
// Handle read responses
_handleReadResponse(_message);
} else {
// Handle regular messages
_handleMessage(_origin, _message);
}
}

function _handleReadResponse(bytes calldata _message) internal {
// Process read response data
uint256 price = abi.decode(_message, (uint256));
// Update application state with fetched data
}

function _handleMessage(Origin calldata _origin, bytes calldata _message) internal {
// Process regular cross-chain messages
string memory data = abi.decode(_message, (string));
// Handle standard messaging logic
}
}

Debugging

lzRead introduces unique challenges compared to standard LayerZero messaging. This comprehensive debugging guide covers common pitfalls, specific error scenarios, and practical solutions to help you troubleshoot lzRead implementations effectively.

Critical Error Scenarios

1. Incorrect Execution Options Type

❌ Problem: Using standard messaging options instead of lzRead-specific options causes transaction reverts.

Root Cause: lzRead requires addExecutorLzReadOption with calldata size estimation, not addExecutorLzReceiveOption.

// ❌ WRONG - Standard messaging options
OptionsBuilder.newOptions().addExecutorLzReceiveOption(100000, 0);

// ✅ CORRECT - lzRead options with size estimation
OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 64, 0);
// gas size value

Solution:

Generate the correct option for use in your enforcedOptions or extraOptions:

import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";

function getReadOptions(uint256 responseSize) internal pure returns (bytes memory) {
return OptionsBuilder
.newOptions()
.addExecutorLzReadOption(
200000, // Gas limit for response processing
responseSize, // Expected response data size in bytes
0 // Native value (usually 0 for reads)
);
}

2. Target Function Reverts (DVN Fulfillment Failure)

❌ Problem: When the target function reverts during execution, DVNs cannot fulfill the request, causing the entire read operation to fail.

Root Cause: DVNs use eth_call to execute target functions. If the function reverts with the provided parameters, verification cannot complete.

Common Revert Scenarios:

  • Invalid parameters passed to target function
  • Target contract state changes between request and execution
  • Insufficient target chain block confirmations
  • Target function has built-in parameter validation that fails
// ❌ PROBLEMATIC - Function may revert with certain parameters
function riskyRead() external payable {
bytes memory callData = abi.encodeWithSelector(
IToken.balanceOf.selector,
address(0) // Zero address may cause revert in some implementations
);
// ... rest of read logic
}

// ✅ SAFE - Validate parameters and use defensive programming
function safeRead(address tokenHolder) external payable {
require(tokenHolder != address(0), "Invalid holder address");
require(tokenHolder.code.length > 0, "Not a contract"); // If targeting contracts

bytes memory callData = abi.encodeWithSelector(
IToken.balanceOf.selector,
tokenHolder
);
// ... rest of read logic
}

Debug Strategy:

// Test target function locally before using in lzRead
function testTargetFunction(address target, bytes memory callData) external view returns (bool success, bytes memory result) {
(success, result) = target.staticcall(callData);
if (!success) {
// Log the revert reason for debugging
if (result.length > 0) {
assembly {
let returndata_size := mload(result)
revert(add(32, result), returndata_size)
}
}
}
}

Prevention Checklist:

  • ✅ Test target function calls with your exact parameters on target chain
  • ✅ Ensure target contract exists at the specified address
  • ✅ Verify function selector matches target contract interface
  • ✅ Check that target function doesn't have restrictive access controls
  • ✅ Test with realistic parameter ranges and edge cases

3. Nonce Ordering Issues (Sequential Verification Failure)

❌ Problem: If the first nonce in a sequence fails, all subsequent nonces are blocked because LayerZero verification is ordered.

Root Cause: LayerZero processes nonces sequentially. A failed or stuck nonce prevents processing of later nonces until resolved.

// ❌ PROBLEMATIC - Multiple rapid requests without nonce management
function multipleReads() external payable {
// These requests will be processed sequentially by nonce
readData(target1, eid1); // Nonce N
readData(target2, eid2); // Nonce N+1 - blocked if N fails
readData(target3, eid3); // Nonce N+2 - blocked if N or N+1 fails
}

Error Indicators:

  • Later transactions succeed but never receive responses
  • LayerZero scan shows "Verified" but not "Delivered" for subsequent messages
  • Nonce gaps in your application's message history

Recovery Strategy:

  1. Identify the failed nonce causing the blockage using nonce status checks
  2. Use endpoint.skip() to bypass the failed nonce and unblock subsequent processing
  3. Ensure subsequent requests are properly formatted and verifiable before sending new reads
  4. Implement prevention by validating all parameters before sending future requests

See the Endpoint - Skip section to see how to skip a nonce.

4. Calldata Size Estimation Errors

❌ Problem: Underestimating response size in addExecutorLzReadOption causes executor delivery failures.

Root Cause: Executors pre-allocate gas based on your size estimate. If the actual response exceeds this size, automatic delivery fails.

// ❌ UNDERESTIMATED - Will fail if response is larger than 32 bytes
OptionsBuilder.newOptions().addExecutorLzReadOption(100000, 32, 0);

// But target function returns: (uint256, address, string) ≈ 100+ bytes
function complexTargetFunction() external view returns (uint256, address, string memory);

Size Calculation Guide:

contract SizeEstimator {
// Calculate response sizes for common types
function estimateResponseSize(bytes memory sampleResponse) external pure returns (uint256) {
return sampleResponse.length;
}

// Common type sizes (for reference):
// uint256: 32 bytes
// address: 32 bytes (padded)
// bool: 32 bytes (padded)
// bytes32: 32 bytes
// string: 32 + length + padding
// dynamic array: 32 + (element_size * length)
// tuple: sum of all element sizes
}

// ✅ PROPER SIZE ESTIMATION
function getOptionsWithCorrectSize(uint256 expectedStringLength) internal pure returns (bytes memory) {
uint256 estimatedSize =
32 + // uint256
32 + // address
32 + // string length
expectedStringLength + // string content
32; // padding buffer

return OptionsBuilder
.newOptions()
.addExecutorLzReadOption(200000, estimatedSize, 0);
}

5. Block Number vs Timestamp on L2 Chains

❌ Problem: Using block.number on L2 chains often references L1 block numbers, causing timing and finality issues.

Root Cause: Many L2s inherit block numbers from their L1 parent chain, making block.number unsuitable for timing-sensitive operations.

Affected Chains:

  • Arbitrum: block.number returns L1 block number, not L2 sequence numbers
  • Optimism: Similar L1 block number inheritance in some contexts
// ❌ PROBLEMATIC on L2s - May reference L1 blocks
EVMCallRequestV1({
// ...
isBlockNum: true,
blockNumOrTimestamp: uint64(block.number), // This is L1 block number on Arbitrum!
// ...
});

// ✅ RECOMMENDED - Use timestamps for universal compatibility
EVMCallRequestV1({
// ...
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp), // Works consistently across all chains
// ...
});

Further Reading