Read External State (LayerZero Read)
LayerZero Read extends the existing omnichain messaging protocol to enable developers to not only send cross-chain messages, but also request and retrieve on-chain state from other supported blockchains.
This new capability transforms how cross-chain data is accessed, manipulated, and computed, allowing developers to offload complex computations from on-chain applications to Decentralized Verifier Networks (DVNs).
Blockchain Query Language
lzRead
is the implementation of LayerZero's Blockchain Query Language — a universal, standardized language for constructing, retrieving, and processing data requests across multiple blockchains and off-chain sources.
lzRead
expands the traditional boundaries of on-chain data, allowing smart contracts to query current and historical states, both locally and externally, including potential Web 2.0 data sources.
Using lzRead
, developers can configure secure, low-latency data queries and manage custom security settings, offering significant flexibility in balancing security, cost, and data accuracy.
The first implementation utilizes the ReadCodecV1
, which initially enables reading calldata
, public
state variables, and functions that do not alter blockchain state (i.e., view
and pure
functions).
As lzRead
evolves, additional capabilities such as querying event logs, private state variables, and even external data sources may be integrated, further extending the versatility of lzRead
as a comprehensive data sourcing utility for on-chain applications.
Workflow
Request Definition: An
OApp
sends alzRead
command throughendpointV2.send()
, specifying the target chain and the block from which the state needs to be retrieved. This command is processed using a custom Send Library (readLib
) that serializes the request and directs it to the appropriate chain via the application's configuredDVN(s)
.DVN Fetch and Return: The
DVN
assigned to the request fetches the requested on-chain data from the target chain. The state is gathered through a node on the destination chain, and optionally processed off-chain via compute logic defined by the application in a targetlzMap()
andlzReduce()
function, before finally being validated by the configuredDVN
threshold in the Receive Library (in this case the samereadLib
).Response Handling (lzReceive): Once the response is verified, the
Executor
delivers the response to theOApp
on the source chain. The same receive workflow used by LayerZero's V2 protocol is triggered viaendpoint.lzReceive()
.Execution and Custom Logic: The
OApp
processes the response data using the logic defined in the application smart contract via an internal_lzReceive()
, allowing the developer to customize how the retrieved state is processed and used.
In this way, lzRead transforms the normal messaging workflow from message delivery to a request
and response
model.
Description | endpoint.send | endpoint.lzReceive | |
---|---|---|---|
Omnichain Message | The bytes sent match the bytes received. | bytes _message | bytes _message |
Omnichain Read | The bytes request differs from the bytes response. | bytes _request | bytes _response |
Instead of sending a source message and using Decentralized Verifier Networks (DVNs) to deliver to the destination, you can now use Decentralized Verifier Networks (DVNs) to read directly from nodes on the destination blockchain.
Supported Chains
lzRead
requires a read compatible Message Library and DVN, meaning you can only use lzRead
on chains with both supported implementations deployed.
You can find these Message Libraries (ReadLib1002
) and Compatible DVNs under "Read Paths".
Additionally, each DVN must also support the target chain to read from.
Installation
To start using LayerZero contracts, you can install the OApp package to an existing project:
- npm
- yarn
- pnpm
- forge
npm install @layerzerolabs/oapp-evm
yarn add @layerzerolabs/oapp-evm
pnpm add @layerzerolabs/oapp-evm
forge install https://github.com/LayerZero-Labs/devtools
forge install https://github.com/LayerZero-Labs/layerzero-v2
Then add to your foundry.toml
:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
'@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/',
'@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol',
]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
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",
}
Usage
To start using lzRead
, you will need to:
Decide what origin chains to deploy your application on and what target data chains to read data from.
Decide which
lzRead
compatible DVNs support your target chains.Deploy an
lzRead
compatible OApp.Set your application's send and receive library to
ReadLib1002
viaendpoint.setSendLibrary()
andendpoint.setReceiveLibrary()
.Set your application's DVN Config via
endpoint.setConfig()
.
Inherit OAppRead
Begin by inheriting the OAppRead.sol
contract in your own smart contract. Familiarize yourself with the OAppRead
implementation to understand how it interacts with LayerZero’s endpoint for both message and read commands.
import { OAppRead } from "./OAppRead.sol";
contract MyOApp is OAppRead {
constructor(address _endpoint, address _delegate) OAppRead(_endpoint, _delegate) {}
}
Decide Messaging Mode
Since lzRead
is an extension to the base messaging protocol, determine the type of cross-chain functionality your app needs:
LayerZero Messaging: For standard cross-chain messages without needing to read external state.
LayerZero Reads (lzRead): To retrieve and use on-chain state from other blockchains.
Hybrid (Both): To implement both messaging and read capabilities.
Here’s an example of how OAppRead
is structured to handle both message-based and read-based responses by implementing a _messageLzReceive
and _readLzReceive
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {OAppRead} from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol";
/// @title OAppRead
/// @notice An example contract that extends OApRead to handle both messaging and read capabilities.
/// @dev Inherits from OApp and adds functionality specific to lzRead.
contract MyOAppMessageAndRead is OAppRead {
/// lzRead responses are sent from arbitrary channels with Endpoint IDs in the range of
/// `eid > 4294965694` (which is `type(uint32).max - 1600`).
uint32 constant READ_CHANNEL_EID_THRESHOLD = 4294965694;
/// @param _endpoint The address of the LayerZero endpoint.
/// @param _delegate The address of the delegate contract.
constructor(address _endpoint, address _delegate) OAppRead(_endpoint, _delegate) {}
/// @notice Internal function to handle incoming messages and read responses.
/// @dev Filters messages based on `srcEid` to determine the type of incoming data.
/// @param _origin The origin information containing the source Endpoint ID (`srcEid`).
/// @param _guid The unique identifier for the received message.
/// @param _message The encoded message data.
/// @param _executor The executor address.
/// @param _extraData Additional data.
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) internal virtual override {
/**
* @dev The `srcEid` (source Endpoint ID) is used to determine the type of incoming message.
* - If `srcEid` is greater than READ_CHANNEL_EID_THRESHOLD (4294965694),
* it corresponds to arbitrary channel IDs for lzRead responses.
* - All other `srcEid` values correspond to standard LayerZero messages.
*/
if (_origin.srcEid > READ_CHANNEL_EID_THRESHOLD) {
// Handle lzRead responses from arbitrary channels.
_readLzReceive(_origin, _guid, _message, _executor, _extraData);
} else {
// Handle standard LayerZero messages.
_messageLzReceive(_origin, _guid, _message, _executor, _extraData);
}
}
/// @notice Internal function to handle standard LayerZero messages.
/// @dev _origin The origin information (unused in this implementation).
/// @dev _guid The unique identifier for the received message (unused in this implementation).
/// @param _message The encoded message data.
/// @dev _executor The executor address (unused in this implementation).
/// @dev _extraData Additional data (unused in this implementation).
function _messageLzReceive(
Origin calldata /* _origin */,
bytes32 /* _guid */,
bytes calldata _message,
address /* _executor */,
bytes calldata /* _extraData */
) internal virtual {
// Implement message handling logic here.
}
/// @notice Internal function to handle lzRead responses.
/// @dev _origin The origin information (unused in this implementation).
/// @dev _guid The unique identifier for the received message (unused in this implementation).
/// @param _message The encoded message data.
/// @dev _executor The executor address (unused in this implementation).
/// @dev _extraData Additional data (unused in this implementation).
function _readLzReceive(
Origin calldata /* _origin */,
bytes32 /* _guid */,
bytes calldata _message,
address /* _executor */,
bytes calldata /* _extraData */
) internal virtual {
// Implement read handling logic here.
}
}
Build Read Requests
To retrieve data from other chains and process it upon receipt, you need to construct read requests and define compute logic. Below is a step-by-step guide on how to construct these read requests, using an example where we read the price of a Uniswap V3 pool token from multiple chains.
Getting the Command
As mentioned above, lzRead uses the same send interface as traditional message passing via endpoint.send
.
To construct the read requests that will be sent to the target chains, you will encode your requests as bytes
and pass the encoding as the bytes _message
parameter. From here, we'll refer to the _message
as the read command, or _cmd
.
Here's an example of how you can initiate the read request:
/**
* @notice Sends a read request to LayerZero, querying Uniswap QuoterV2 for WETH/USDC prices on configured chains.
* @param _extraOptions Additional messaging options, including gas and fee settings.
* @return receipt The LayerZero messaging receipt for the request.
*/
function readAverageUniswapPrice(
bytes calldata _extraOptions
) external payable returns (MessagingReceipt memory receipt) {
bytes memory cmd = getCmd();
return
_lzSend(
READ_CHANNEL,
cmd,
combineOptions(READ_CHANNEL, READ_MSG_TYPE, _extraOptions),
MessagingFee(msg.value, 0),
payable(msg.sender)
);
}
The getCmd()
function is responsible for building the command that includes both the read requests and any compute instructions.
For example, this getCmd()
creates multiple read requests and a compute request:
/**
* @notice Constructs a command to query the Uniswap QuoterV2 for WETH/USDC prices on all configured chains.
* @return cmd The encoded command to request Uniswap quotes.
*/
function getCmd() public view returns (bytes memory) {
uint256 pairCount = targetEids.length;
EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](pairCount);
for (uint256 i = 0; i < pairCount; i++) {
uint32 targetEid = targetEids[i];
ChainConfig memory config = chainConfigs[targetEid];
// Define the QuoteExactInputSingleParams
IQuoterV2.QuoteExactInputSingleParams memory params = IQuoterV2.QuoteExactInputSingleParams({
tokenIn: config.tokenInAddress,
tokenOut: config.tokenOutAddress,
amountIn: 1 ether, // amountIn: 1 WETH
fee: config.fee,
sqrtPriceLimitX96: 0 // No price limit
});
// @notice Encode the function call
// @dev From Uniswap Docs, this function is not marked view because it relies on calling non-view
// functions and reverting to compute the result. It is also not gas efficient and should not
// be called on-chain. We take advantage of lzRead to call this function off-chain and get the result
// returned back on-chain to the OApp's _lzReceive method.
// https://docs.uniswap.org/contracts/v3/reference/periphery/interfaces/IQuoterV2
bytes memory callData = abi.encodeWithSelector(IQuoterV2.quoteExactInputSingle.selector, params);
readRequests[i] = EVMCallRequestV1({
appRequestLabel: uint16(i + 1),
targetEid: targetEid,
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp),
confirmations: config.confirmations,
to: config.quoterAddress,
callData: callData
});
}
EVMCallComputeV1 memory computeSettings = EVMCallComputeV1({
computeSetting: 2, // lzMap() and lzReduce()
targetEid: ILayerZeroEndpointV2(endpoint).eid(),
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp),
confirmations: 15,
to: address(this)
});
return ReadCodecV1.encode(0, readRequests, computeSettings);
}
Following best practices, you should create a dedicated function to construct your specific command requests.
This involves creating an array of EVMCallRequestV1
structs, each representing a read operation on a specific chain and contract.
_lzSend
is an internal virtual
method provided in the OApp
contract standard, which can be used to invoke the endpoint.send
if certain security checks pass:
function _lzSend(
uint32 _dstEid,
bytes memory _message,
bytes memory _options,
MessagingFee memory _fee,
address _refundAddress
) internal virtual returns (MessagingReceipt memory receipt) {
// @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint.
uint256 messageValue = _payNative(_fee.nativeFee);
if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee);
return
// solhint-disable-next-line check-send-result
endpoint.send{ value: messageValue }(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0),
_refundAddress
);
}
If using the OApp
standard, consider _lzSend
the recommended way to call endpoint.send
.
Call Requests
Each EVMCallRequestV1
struct represents a read request and includes the following fields:
readRequests[i] = EVMCallRequestV1({
appRequestLabel: uint16(i + 1),
targetEid: targetEid,
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp),
confirmations: config.confirmations,
to: config.quoterAddress,
callData: callData
});
Name | Type | Description |
---|---|---|
appRequestLabel | uint16 | A label to identify the request within your application logic. Useful for tracking response types. |
targetEid | uint32 | The Endpoint ID of the target chain where the data is to be read from. |
isBlockNum | bool | A boolean indicating whether blockNumOrTimestamp is a block number (true ) or a timestamp (false ). |
blockNumOrTimestamp | uint64 | Specifies the block.number or block.timestamp on the target chain (targetEid ) at which to read the state. |
confirmations | uint16 | The number of confirmations required to wait for the block or timestamp finality. |
to | address | The target contract address on the destination chain (e.g., the Uniswap V3 Quoter contract). |
callData | bytes | The ABI-encoded function calldata. |
The callData
field contains the encoded function call that will be executed on the target contract.
For example, IQuoterV2.quoteExactInputSingle.selector
specifies the quoteExactInputSingle
function in the Uniswap V3 QuoterV2
contract to request a price quote for swapping tokenIn
to tokenOut
on the specified Uniswap V3 pool.
The EVMCallRequestV1
struct is limited to chain calldata
because it encodes function calls into a format that can be transmitted and understood across chains using the EVM's ABI encoding.
This limitation means that lzRead
can only handle data types compatible with calldata
, specifically value types and fixed-size data structures.
Compute Logic
You can also specify compute instructions to process the retrieved data after it's read. This is done using the EVMCallComputeV1
struct.
EVMCallComputeV1 memory computeSettings = EVMCallComputeV1({
computeSetting: 2, // lzMap() and lzReduce()
targetEid: ILayerZeroEndpointV2(endpoint).eid(),
isBlockNum: false,
blockNumOrTimestamp: uint64(block.timestamp),
confirmations: 15,
to: address(this)
});
Name | Type | Description |
---|---|---|
computeSetting | uint8 | Specifies the compute operations to perform, such as lzMap (0), lzReduce , (1), or map and reduces (2). More on this later. |
targetEid | uint32 | The Endpoint ID of the target chain where the lzMap or lzReduce implementation can be read from. |
isBlockNum | bool | A boolean indicating whether blockNumOrTimestamp is a block number (true ) or a timestamp (false ). |
blockNumOrTimestamp | uint64 | Specifies the block.number or block.timestamp on the target chain at which to read the state. |
confirmations | uint16 | The number of confirmations required to wait for the block or timestamp finality. |
to | address | The target contract address to view lzMap and / or lzReduce . |
Note: The next section will dive deeper into how to define the compute logic within lzMap
and lzReduce
.
Encoding Command
Once you have defined the command's EVMCallRequestV1
and the EVMCallComputeV1
, you can use the CmdCodecV1
to encode the command to end your getter function.
int16 appLabel = 0; // Application label (set as needed)
// Encoding the command
return CmdCodecV1.encode(appLabel, readRequests, evmCompute);
Different chains have different interpretations of block.number
and block.timestamp
.
For example, when calling block.number
on Arbitrum L2 the value returned is the block number on Ethereum L1, rather than block.number
of the Arbitrum chain itself.
Consider how these edge cases specific to each targetEid
impact your application before finalizing the encoding process for your command from EVMCallRequestV1
and EVMCallComputeV1
.
Declare Compute Logic
Compute logic is executed off-chain via your application's configured Decentralized Verifier Networks (DVNs). To define specific compute logic, you must target a contract that implements either IOAppMapper
or IOAppReducer
, which define the lzMap()
and lzReduce()
functions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title IOAppMapper Interface
/// @notice Defines the lzMap function for mapping operations
interface IOAppMapper {
/**
* @notice Processes a single mapping operation
* @param _request The request data in bytes
* @param _response The response data in bytes
* @return A bytes array resulting from the mapping operation
*/
function lzMap(
bytes calldata _request,
bytes calldata _response
) external view returns (bytes memory);
}
/// @title IOAppReducer Interface
/// @notice Defines the lzReduce function for reducing operations
interface IOAppReducer {
/**
* @notice Processes a reduction operation over multiple responses
* @param _cmd The command data in bytes
* @param _responses An array of response data in bytes
* @return A bytes array resulting from the reduction operation
*/
function lzReduce(
bytes calldata _cmd,
bytes[] calldata _responses
) external view returns (bytes memory);
}
These optional view
or pure
functions enable your DVNs to read and compute state changes based on the _response
or _responses
from each request.
Compute Settings
When you declare an EVMCallComputeV1
, you also select a compute setting for whether your configured DVNs should mutate the _response
data using lzMap()
(0
), lzReduce()
, (1
), both lzMap()
and lzReduce()
(2
), or no compute setting (3
).
uint8 internal constant MAP_ONLY = 0;
uint8 internal constant REDUCE_ONLY = 1;
uint8 internal constant MAP_AND_REDUCE = 2;
uint8 internal constant NONE = 3;
EVMCallComputeV1 memory evmCompute = EVMCallComputeV1({
computeSetting: MAP_AND_REDUCE, // lzMap() and lzReduce()
targetEid: eid, // Current chain's EID
isBlockNum: true,
blockNumOrTimestamp: uint64(block.number),
confirmations: 2,
to: address(this) // Compute executed in this contract
});
If only
lzMap()
, the returned data toOApp._lzReceive()
will be the concatenation of every return output for each_request
yourlzMap()
processes. You will need to track theindex
of each request made in the command to later decode in your receive logic.If only
lzReduce
, thelzReduce()
implementation will intake the concatenation in order of how theEVMCallRequestV1[]
were made as abytes
argument, and return a singlebytes
output. You can uselzReduce()
to aggregate all responses.If both, data will be manipulated on a per request level first via
lzMap()
and an aggregate level vialzReduce()
, before being returned toOApp._lzReceive()
.
Implementation details can be found below.
Mapping Requests
Mapping refers to defining how to format or process the data returned by each individual request. In the Uniswap example, the lzMap()
simplifies the returned data by only decoding the amountOut
for use in the lzReduce()
step.
/**
* @notice Processes individual Uniswap QuoterV2 responses, encoding the result.
* @param _response The response from the read request.
* @return Encoded token output amount (USDC amount).
*/
function lzMap(bytes calldata, bytes calldata _response) external pure returns (bytes memory) {
require(_response.length >= 32, "Invalid response length"); // quoteExactInputSingle returns multiple values
// Decode the response to extract amountOut
(uint256 amountOut, , , ) = abi.decode(_response, (uint256, uint160, uint32, uint256));
return abi.encode(amountOut);
}
The decoding above only decodes to demonstrate how an lzMap()
can intake a _response
, mutate the returned bytes
per _request
, and return a new encoded output depending on the needs of your application.
Reducing Requests
Once you've mapped the individual responses, the lzReduce()
function aggregates these mapped responses to produce a final result.
In the Uniswap example, lzReduce()
calculates the average amountOut
from all responses.
/**
* @notice Aggregates individual token output amounts to compute an average.
* @param _responses Array of mapped responses containing token output amounts.
* @return Encoded average token output amount.
*/
function lzReduce(bytes calldata, bytes[] calldata _responses) external pure returns (bytes memory) {
require(_responses.length == 3, "Expected responses from 3 chains");
uint256 total = 0;
for (uint256 i = 0; i < _responses.length; i++) {
uint256 amountOut = abi.decode(_responses[i], (uint256));
total += amountOut;
}
uint256 averageAmountOut = total / _responses.length;
return abi.encode(averageAmountOut);
}
Creating Request Options
lzRead
uses a new options type, addExecutorLzReadOption
to send requests for target data.
OptionsBuilder.newOptions().addExecutorLzReadOption(100_000, 64, 0)
If unfamiliar with _options
, you can read the full scope in Execution Options, but for reference this send parameter allows you to deliver an amount of gasLimit
automatically to endpoint.lzReceive
from your configured Executor.
For lzRead
, an additional requirement for _options
is to profile the calldata
size of your returned data type:
OptionsBuilder.newOptions().addExecutorLzReadOption(GAS_LIMIT, CALLDATA_SIZE, MSG_VALUE)
These _options
can be enforced as usual by inheriting the Enforced Options Helper in your OAppRead
contract:
import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol";
import { OAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
contract MyOAppRead is OAppRead, OAppOptionsType3 {}
Adding other _options
in the endpoint.send
call to a channelId
will cause the transaction to revert on source.
Receive Responses
Finally, you need to define your internal handler for incoming messages from the LayerZero protocol. This is where you'll process the final aggregated result and use it within your contract.
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address _executor,
bytes calldata _extraData
) internal
In the Uniswap example, the handler processes the message and emits an event with the average token price:
/// @notice Emitted when the average amount out is computed and received.
event AggregatedPrice(uint256 averageAmountOut);
/**
* @notice Handles the aggregated average price from Uniswap V3 pool responses received from LayerZero.
* @dev Emits the AggregatedPrice event with the calculated average amount.
* @param _message Encoded average token output amount.
*/
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
require(_message.length == 32, "Invalid message length");
uint256 averagePrice = abi.decode(_message, (uint256));
emit AggregatedPrice(averagePrice);
}
By using lzRead
, you can drastically simplify the amount of logic needed for updating state on-chain.
Setting Libraries and DVNs
Set your configuration for the Read Library and required DVNs that support lzRead
. See all available DVNs.
You can set your DVN configuration either via the LayerZero CLI tool or via example scripts.
Setting Read Channel
Once your configuration has been applied, simply call setReadChannel
for the specific channel configuration to set and begin sending messages!
Channels let you define multiple types of read logic per application. You can specify per request label, per application label, or per channel.
// OAppRead.sol
// Only Owner
function setReadChannel(uint32 _channelId, bool _active) public virtual onlyOwner {
_setPeer(_channelId, _active ? AddressCast.toBytes32(address(this)) : bytes32(0));
}
To see the available channels for each target data chain, see Read Paths.
Debugging Read Commands
When working with lzRead
, you might encounter issues where read commands (Requests and Compute logic) fail to execute as expected. LayerZero provides tools and best practices to help you debug these scenarios effectively.
When checking LayerZero Scan for the read status, you may encounter the following errors.
Malformed Command
Malformed indicates that the read command was constructed incorrectly and does not adhere to the required serialization format. This can be due to:
Incorrect packing of the command parameters.
Errors in encoding the
calldata
.Misuse of the
ReadCodecV1.sol
library during command construction.
To resolve a Malformed Command, review how the command was packed on the origin chain by examining your getCmd
implementation. Ensure that you are correctly using the ReadCodecV1.sol
library for serialization.
Unresolvable Command
The target data chain cannot fulfill the read request due to issues related to invoking the calldata
on the target chain, such as:
Contract Address Issues:
The specified contract does not exist on the target chain.
The contract address is incorrect for the given block number or timestamp.
Method Identifier Issues:
- The calldata contains an invalid or non-existent method selector.
State Issues:
- The contract is deployed, but the state at the specified
block.number
orblock.timestamp
does not support the requested operation.
To resolve an Unresolvable Command:
Ensure that the contract exists at the specified address on the target chain.
Verify that the contract is deployed and initialized correctly.
Double-check the contract address and ensure it matches the target chain's deployment.
Confirm that the
block.number
orblock.timestamp
specified in the read request corresponds to a state where the contract is available.Ensure that the method selector used in the
calldata
exists in the target contract.Verify that the function signatures match and that the target contract implements the requested methods.
You can use the Read CLI Debugging Task to help with the debugging process by ensuring the command decodes and reads the target data chain correctly.