Batch Contract Reads with Multicall3 + Viem

Batch Contract Reads with Multicall3 + Viem
Photo by Pavan Trikutam / Unsplash

The most common use case for using multicall is to allow a single eth_call JSON RPC request to return results of multiple contract function calls (batch read)

The benefits include:

  • Reduces the number of separate JSON RPC requests; instead of multiple requests with a sequence, we call only a single request.
  • All values returned are from the same block.
Read more information for multicall: https://www.multicall3.com/

Contract ABI

[
  "function aggregate(tuple(address target, bytes callData)[] calls) payable returns (uint256 blockNumber, bytes[] returnData)",
  "function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)",
  "function aggregate3Value(tuple(address target, bool allowFailure, uint256 value, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)",
  "function blockAndAggregate(tuple(address target, bytes callData)[] calls) payable returns (uint256 blockNumber, bytes32 blockHash, tuple(bool success, bytes returnData)[] returnData)",
  "function getBasefee() view returns (uint256 basefee)",
  "function getBlockHash(uint256 blockNumber) view returns (bytes32 blockHash)",
  "function getBlockNumber() view returns (uint256 blockNumber)",
  "function getChainId() view returns (uint256 chainid)",
  "function getCurrentBlockCoinbase() view returns (address coinbase)",
  "function getCurrentBlockDifficulty() view returns (uint256 difficulty)",
  "function getCurrentBlockGasLimit() view returns (uint256 gaslimit)",
  "function getCurrentBlockTimestamp() view returns (uint256 timestamp)",
  "function getEthBalance(address addr) view returns (uint256 balance)",
  "function getLastBlockHash() view returns (bytes32 blockHash)",
  "function tryAggregate(bool requireSuccess, tuple(address target, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)",
  "function tryBlockAndAggregate(bool requireSuccess, tuple(address target, bytes callData)[] calls) payable returns (uint256 blockNumber, bytes32 blockHash, tuple(bool success, bytes returnData)[] returnData)",
]

Contract Address for Multicall3

0xcA11bde05977b3631167028862bE2a173976CA11
multicall
Batches up multiple functions on a contract in a single call.

Viem have built-in multicall

Fetch user balance

We will create a script to fetch the user balance with a list of tokens. For example, if we have 20 lists of ERC20 tokens, if we are not using multicall, we need to call eth_call 20 requests to get a result, not only multiple requests, but also need to wait for each request, or exceed the rate limit, or some RPCs do not support concurrent requests

Create a client with Viem on mainnet

import {
  createPublicClient,
  http,
  formatUnits,
  parseAbi,
  type Address,
} from 'viem'
import { mainnet } from 'viem/chains'

export const publicClient = createPublicClient({
  chain: mainnet,
  transport: http()
})

Example ABI

const erc20Abi = parseAbi([
  'function balanceOf(address _owner) view returns (uint256 balance)',
  'function decimals() view returns (uint8)',
  'function symbol() view returns (string)',
])

List of example tokens

interface Token {
  address: Address
  symbol: string
  decimals: number
}

interface TokenBalance {
  symbol: string
  address: Address
  balance: string
  rawBalance?: string
  error?: string
}

// Popular ERC-20 tokens on Ethereum mainnet
const tokens: Token[] = [
  {
    address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
    symbol: 'USDC',
    decimals: 6,
  },
  {
    address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
    symbol: 'USDT',
    decimals: 6,
  },
  {
    address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
    symbol: 'WBTC',
    decimals: 8,
  },
  {
    address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
    symbol: 'WETH',
    decimals: 18,
  },
  {
    address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52',
    symbol: 'BNB',
    decimals: 18,
  },
]

Fetch user wallet to get balance of tokens (assume we have 20+ ERC20 tokens)

  // This makes 20 parallel HTTP requests
  const balances = await Promise.all(
    tokens.map(token => client.readContract({
      address: token.address,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [walletAddress],
    }))
  )

Example maybe issue with rate limit it 20 request per sec

Instead of calling with multiple requests, we use multicall to batch the function with SINGLE eth_call

async function getTokenBalances(
  walletAddress: Address,
): Promise<TokenBalance[]> {
  console.log(`Getting token balances for wallet: ${walletAddress}\n`)

  const balances = await Promise.all(
    tokens.map(async (token) => {
      try {
        const balance = await client.readContract({
          address: token.address,
          abi: erc20Abi,
          functionName: 'balanceOf',
          args: [walletAddress],
        })

        const formattedBalance = formatUnits(balance, token.decimals)

        return {
          symbol: token.symbol,
          address: token.address,
          balance: formattedBalance,
          rawBalance: balance.toString(),
        }
      } catch (error) {
        return {
          symbol: token.symbol,
          address: token.address,
          balance: 'Error',
          error: error.message,
        }
      }
    }),
  )

  return balances
}

Try to run it:

bun run get-balances.ts

Result

Getting token balances for wallet: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Token Balances:
===============
USDC: 17057.40778
USDT: 260.072995
WBTC: 0.00107182
WETH: 0.0000001
BNB: 0.02

Source Code

get-balances.ts
GitHub Gist: instantly share code, notes, and snippets.

References

a single