Skip to main content

Overview

Declared eth_calls are a performance optimization technique that allows you to pre-declare contract calls in your subgraph manifest. Instead of making synchronous RPC calls during event processing, the indexer executes these calls ahead of time and caches the results in memory. This dramatically improves indexing performance by eliminating RPC latency from your handler execution.
Declared eth_calls can reduce indexing time by up to 10x for subgraphs that make frequent contract calls during event processing.

How it works

When you declare an eth_call in your subgraph manifest, the indexing engine follows this optimized workflow:
  1. Pre-execution: Before processing events, the indexer identifies all declared eth_calls for the current block
  2. Batch fetching: All declared calls are executed in parallel against the blockchain
  3. In-memory caching: Results are stored in an efficient in-memory cache
  4. Handler access: When your event handler code binds to a contract and makes a call, it retrieves the cached result instantly instead of making an actual RPC call
This approach transforms slow, synchronous RPC calls into fast memory lookups, significantly reducing indexing time.

Requirements

Declared eth_calls require specVersion: 1.2.0 or higher in your subgraph manifest.
To use declared eth_calls, ensure your subgraph.yaml specifies the correct version:
subgraph.yaml
specVersion: 1.2.0
schema:
  file: ./schema.graphql

Syntax

Declared eth_calls are added as a calls property on an event handler entry in your subgraph manifest. The calls property is an object where each key is an arbitrary name and the value describes the eth_call to make. The general syntax is:
eventHandlers:
  - event: YourEvent(...)
    handler: handleYourEvent
    calls:
      callName: ABI[address].method(arg1, arg2, ...)
Breaking this down:
  • calls: — an object of key-value pairs nested under the event handler that triggers the eth_calls.
  • callName: — an arbitrary name you choose for the declared call. Use something self-documenting, e.g. if your eth_call is getLiquidity, name it getLiquidity: ....
  • ABI — the name of the ABI as defined in the abis section of your manifest. This corresponds to how you use ABI.bind(address) in your mapping code. You can search your source mapping for .bind( calls to identify where declared eth_calls can be applied.
  • [address] — the address passed to the ABI when binding. This can be event.address, event.params.<name>, or a constant hex address.
  • .method(...) — the contract method to call. Use the actual method name even if your mapping uses try_ syntax (e.g. if your code calls try_getSymbol, declare it as .getSymbol()).
  • (args) — the arguments passed to the contract method. These can be event.address, event.params.<name>, or constant values.
Constants like hex addresses are embedded without quotes. Make sure to follow the pattern shown in the examples below.

Implementation guide

Step 1: Identify candidate calls

Look for contract calls in your event handlers that:
  • Are called frequently during event processing
  • Access the same contract method repeatedly
  • Contribute to slow indexing performance
Common examples include:
  • Getting token metadata (name, symbol, decimals)
  • Checking user balances or allowances
  • Reading contract configuration values
  • Fetching pool or pair addresses from a factory

Step 2: Declare the calls in your manifest

Add a calls property to the relevant event handler. Here’s an example for a Uniswap V3 pool that pre-fetches token0 and token1 addresses on every swap:
subgraph.yaml
dataSources:
  - kind: ethereum/contract
    name: UniswapV3Pool
    network: mainnet
    source:
      address: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"
      abi: UniswapV3Pool
      startBlock: 12369621
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      entities:
        - Swap
        - Pool
      abis:
        - name: UniswapV3Pool
          file: ./abis/UniswapV3Pool.json
        - name: ERC20
          file: ./abis/ERC20.json
      eventHandlers:
        - event: Swap(indexed address,indexed address,int256,int256,uint160,uint128,int24)
          handler: handleSwap
          calls:
            # Pre-fetch token0 and token1 addresses from the pool contract
            token0: UniswapV3Pool[event.address].token0()
            token1: UniswapV3Pool[event.address].token1()
      file: ./src/mapping.ts

Step 3: Use cached results in your event handler

No separate handler functions are needed for declared eth_calls. The results are automatically cached and returned when your event handler binds to the contract and calls the same method. Your existing mapping code works as-is:
src/mapping.ts
import { Address } from '@graphprotocol/graph-ts'
import {
  Swap,
  UniswapV3Pool
} from '../generated/UniswapV3Pool/UniswapV3Pool'
import { Pool, Token } from '../generated/schema'

export function handleSwap(event: Swap): void {
  let poolAddress = event.address
  let pool = Pool.load(poolAddress.toHex())

  if (!pool) {
    pool = new Pool(poolAddress.toHex())

    // Bind to contract and make calls
    // These will use the cached results from declared eth_calls
    let poolContract = UniswapV3Pool.bind(poolAddress)

    let token0Result = poolContract.try_token0()
    let token1Result = poolContract.try_token1()

    if (!token0Result.reverted) {
      pool.token0 = token0Result.value.toHex()
    }

    if (!token1Result.reverted) {
      pool.token1 = token1Result.value.toHex()
    }
  }

  // Process swap data
  pool.volume = pool.volume.plus(event.params.amount0)
  pool.save()
}
Note that even though the mapping uses try_token0, the declared call uses just token0 — omit the try_ prefix in the manifest.

Address and argument patterns

The calls syntax supports several ways to specify the contract address and method arguments. You can use event.address, event.params.<name>, or constant hex addresses in any combination:
eventHandlers:
  - event: Created(address)
    handler: handleCreated
    calls:
      # Use event.address as the contract address, event param as argument
      fromEventAddr: Factory[event.address].get(event.params.address)

      # Use an event param as the contract address
      fromParam: Factory[event.params.address].get(event.params.address)

      # Use a constant address, pass event.address as argument
      fromConstant: Factory[0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF].get(event.address)

      # Both the address and argument are constants
      allConstants: Factory[0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF].get(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF)
Constants are embedded without quotes — no string wrapping needed.

Best practices

Each declared call adds overhead during the pre-execution phase. Only declare calls that your event handlers will actually use during event processing.
Always use the try_ prefix when calling contract methods in your handlers. This ensures your subgraph continues indexing even if a call fails or reverts. The declared call in the manifest always uses the base method name (without try_).
let result = contract.try_balanceOf(userAddress)
if (!result.reverted) {
  entity.balance = result.value
}
Declared eth_calls are most effective for calls that happen frequently. If a call only happens once per block or less, the overhead of declaring it may outweigh the benefits.
The address and arguments in your declared call must match what your event handler actually passes when binding and calling the contract method. If they don’t match, the cache won’t be used and a standard RPC call will be made instead.

Performance impact

The performance benefits of declared eth_calls depend on several factors:
  • Call frequency: More frequent calls see greater benefits
  • RPC latency: Higher latency networks benefit more from caching
  • Handler complexity: Handlers with multiple contract calls see the most improvement
In practice, subgraphs with heavy RPC usage can see indexing speed improvements of 5-10x when properly implementing declared eth_calls.
After implementing declared eth_calls, you should see faster indexing speeds and reduced RPC load in your deployment metrics.

Example implementation

Check out our complete example implementation showing declared eth_calls in action with an ERC-20 token subgraph on the Taiko network.

Troubleshooting

Ensure your specVersion is set to 1.2.0 or higher. Older spec versions don’t support declared eth_calls and will fall back to standard synchronous calls.
Verify that the address and arguments in your declared call match exactly what your event handler passes. For example, if you declare Factory[event.address].get(event.params.address) but your handler binds to a different address, the cached result won’t be used.
Check that the contract ABI includes all the functions you’ve declared. Missing function definitions will cause indexing to fail.