ERC-3668: CCIP Read — Verifiable Off-chain Data
ERC-3668 (also called "CCIP Read") standardises the round-trip pattern for contracts that need to read data hosted off-chain: an L2, an IPFS gateway, a centralised API. The contract reverts with a structured OffchainLookup error containing URLs and a callback selector; a CCIP-Read-aware client fetches the data from the URLs and resubmits the call to the callback, which verifies the data and uses it. ENS uses CCIP Read for off-chain name resolution; Chainlink CCIP uses it for cross-chain message proofs.
Required Pattern
error OffchainLookup(
address sender, // contract that emitted the error
string[] urls, // URLs the client should try
bytes callData, // payload to POST / append to GET
bytes4 callbackFunction,
bytes extraData // opaque state passed to the callback
);
function name(bytes32 node) external view returns (string memory) {
bytes memory data = abi.encode(node);
string[] memory urls = new string[](1);
urls[0] = "https://offchain.ens.domains/lookup/{sender}/{data}.json";
revert OffchainLookup(
address(this), urls, data,
this.nameCallback.selector, abi.encode(node)
);
}
function nameCallback(bytes calldata response, bytes calldata extraData)
external view returns (string memory)
{
(bytes32 node) = abi.decode(extraData, (bytes32));
(string memory name, bytes memory signature) = abi.decode(response, (string, bytes));
require(_verifySignature(node, name, signature), "bad signature");
return name;
}The data is verified on-chain (signature, merkle proof, etc.) — CCIP Read is just a client-side resolution protocol; trust still rooted at the contract.
Neo Equivalent: Native Oracle Service
Neo bakes off-chain data retrieval into the protocol via the Oracle native contract — a different shape than CCIP Read, but the same end goal: a contract requests off-chain data, oracle nodes fetch it, the contract gets a callback with the response.
// Request side — contract initiates the lookup.
Oracle.Request(
"https://api.example.com/data?key=foo", // URL
"$.value", // JSONPath filter (server-side extract)
"OnOracleResponse", // callback method name
null, // user data (round-trips back)
Oracle.MinimumResponseFee // GAS budget
);
// Callback — invoked by the oracle native contract with the result.
[DisplayName("OnOracleResponse")]
public static void OnOracleResponse(string url, byte[] userData, OracleResponseCode code, byte[] result)
{
if (Runtime.CallingScriptHash != Oracle.Hash) throw new Exception("not oracle");
if (code != OracleResponseCode.Success) throw new Exception("oracle failed");
// result is already JSONPath-filtered to the relevant value
Storage.Put(Storage.CurrentContext, "last", result);
}| ERC-3668 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
revert OffchainLookup(...) | Oracle.Request(url, filter, callback, userData, gas) | Neo dispatches the request directly; no client-side resolution |
| URLs in the revert payload | Single URL per request (or HTTPS / IPFS depending on policy) | Multiple URLs require multiple requests |
| Client fetches off-chain | Oracle nodes (consensus participants) fetch | Trust model: oracle quorum, not client |
| Verify signature in callback | Trust the oracle quorum (or layer your own verification) | Optional app-level signature still works |
extraData round-tripped | userData round-tripped via the oracle native contract | Same affordance |
| JSON parsing in Solidity | JSONPath filter applied server-side by the oracle | Faster, cheaper, no on-chain JSON parser needed |
Why The Trust Model Differs
CCIP Read leaves trust in the contract: the off-chain server is untrusted; the contract verifies the result before using it. Neo's oracle leaves trust in the oracle quorum (consensus participants who fetch the URL): the contract trusts that a majority of oracle nodes returned the same answer. For high-value reads, layer signature verification on top — the off-chain endpoint signs the response, the contract verifies the signature in the callback regardless of which oracle node delivered it.
Migration Notes
Solidity contracts using CCIP Read can be ported to Neo via neo-solc by:
- Replacing the
OffchainLookuprevert with a constructor parameter or storage slot pointing at the oracle URL. - Replacing the lookup function with a thin wrapper that calls
Oracle.Request(...). - Renaming the callback to whatever Neo dispatch expects (the method name passed to
Oracle.Request). - Optionally retaining the in-callback signature verification — useful when the oracle service is acting as transport but the data integrity is guaranteed by the source's signature.
For ENS-style off-chain name resolution, the result is roughly equivalent in latency (one block vs. one HTTPS round-trip) but the on-chain GAS cost is lower on Neo because there's no Solidity-side JSON parsing — the oracle filters server-side via JSONPath before delivering.
