Batch Contract Reads with Multicall3 + Viem
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

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

References
- Viem multicall - https://viem.sh/docs/contract/multicall#multicall
- Multicall repo - https://github.com/mds1/multicall3
a single