How to Decode Ethereum Calldata Without an ABI File

You have a contract address and a raw 0x calldata blob, no ABI file. Here's how to decode the function call and its arguments anyway, with a worked example.

evmquery team··7 min read
Share
Decoding Ethereum calldata into a function call and arguments without an ABI file

You’re looking at a transaction’s input data: a 0x-prefixed hex blob a few hundred characters long, sent to a contract address you may or may not recognize. You need to know what function it calls and what arguments it passes. What you don’t have is the contract’s ABI, and every calldata decoder you’ve tried so far wants you to paste one in before it will do anything.

TL;DR

Calldata alone can’t tell you what function it calls, its 4-byte selector is a one-way hash. You either need the ABI already, or you need to resolve it from the contract address first. evmquery’s Calldata Decoder does the resolution automatically, verified source, then code-reuse and signature matching, following proxies, and decodes against whatever it finds. Paste an address and calldata, get a typed function call back.

The problem: an address and some bytes, nothing else

This situation is more common than it looks like it should be:

  • You’re watching a mempool feed and see a transaction headed for a contract you haven’t integrated with.
  • A multisig queue shows a target address and a hex blob waiting for signatures.
  • A transaction reverted, and before you chase the revert reason you want to confirm the call you built is the call you meant to build.
  • The contract is unverified on the block explorer, so the explorer’s own decoder shows Method ID: 0x... and stops there.
  • The address is a proxy, and the ABI you’d need lives on an implementation contract you haven’t looked up yet.

In every one of these, you have the two inputs a decoder needs least, an address and some bytes, and not the one it actually wants: a matching ABI.

Why you can’t decode a selector back into a function

Calldata has exactly two parts. The first four bytes are the function selector, the leading four bytes of the keccak256 hash of the function’s canonical signature. transfer(address,uint256) hashes down to 0xa9059cbb. Everything after those four bytes is the ABI-encoded argument list, packed into 32-byte words.

The second part is mechanical to decode once you know the types involved; any ABI library handles it. The first part is the actual obstacle. A hash only goes one way. You cannot take 0xa9059cbb and derive transfer(address,uint256) from it. You either already know the signature, or you look the selector up somewhere and hope the answer is right for this specific contract.

That “hope” is where things go wrong. Signature databases like 4byte and OpenChain are crowd-sourced indexes of selector-to-signature mappings, and a 4-byte selector is short enough that unrelated functions collide on the same one. A lookup can hand you a plausible-looking signature that isn’t what the contract in front of you actually implements, decode the arguments against it anyway, and give you numbers that look fine and are wrong.

Why “paste your ABI” tools don’t solve the actual problem

Most calldata decoders on the market, bia.is’s ABI decoder and solarity.dev’s decoder among them, are built around a text area where you paste the contract’s ABI JSON first. Once you have it, decoding the calldata against it is the easy, solved half of the job. Finding the right ABI for an unverified contract, a proxy, or one of forty near-identical deployments of the same protocol is the hard half, and it’s the half these tools hand back to you.

If you already have the ABI sitting in a file, that’s fine, any of those tools will decode against it correctly. The situation this post is about is the one where you don’t: you have an address and some bytes, and getting from there to an ABI is itself the work.

Resolving the ABI from the address instead

The fix is to invert the order: resolve the ABI from the contract address first, then use the resolved method set as the only candidates a selector match is checked against. That’s a materially different search space. Instead of “every signature anyone has ever uploaded to a public database,” the candidates become “the methods this specific contract actually exposes,” which removes most of the collision risk a bare 4byte lookup carries.

This is the same resolution Contract Inspector uses, and it’s covered in full in how evmquery resolves a contract read: verified source first (Sourcify, Etherscan), then code-reuse matching against known bytecode, IPFS metadata, known interfaces, and signature databases, following EIP-1967, UUPS, Beacon, EIP-1167, EIP-2535 diamond, Gnosis Safe, and EIP-7702 proxy patterns to the implementation that actually holds the logic. A proxy’s own bytecode is usually a thin forwarder with almost nothing decodable in it; the real ABI lives on whatever it delegates to.

What resolution-first decoding gets you

  • The candidate set for a selector match is “methods this contract exposes,” not “every signature ever indexed,” so overloads and cross-contract selector collisions resolve to the right function instead of a plausible-sounding wrong one.
  • Proxy addresses decode against the implementation’s ABI automatically. You never manually read an EIP-1967 storage slot to find it.
  • Each resolved method reports where its ABI came from (verified source vs. a signature database), so you can tell a high-confidence decode from a best-effort one.

Worked example: decoding a USDC balance check

Here’s what that looks like end to end, using evmquery’s Calldata Decoder against USDC on Ethereum (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) and this calldata, a balance check against a Binance hot wallet:

0x70a0823100000000000000000000000028c6c06298d514db089934071355e5743bf21d60

Three fields go into the tool: the contract address, the chain (Ethereum), and the calldata above. No ABI field exists to fill in, because the tool doesn’t need one from you.

The result panel decodes it as:

  • Function: balanceOf(address)
  • Selector: 0x70a08231
  • Executes at: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (USDC’s proxy resolves to its implementation before matching runs)
  • Source: verified source
  • Arguments: account = 0x28C6c06298d514Db089934071355E5743bf21d60

That single argument is the address whose balance the call reads, in this case a known Binance hot wallet. Nothing about the lookup is specific to the tool, it’s what any ABI decoder does once it has the right types, the only step this replaces is finding those types in the first place.

Build it yourself

The tool’s result panel is a thin layer over two steps you can run directly: resolve the ABI over REST, then decode locally with an ABI library. The example below repeats the USDC balance check from above.

Resolve the ABI

curl -X POST https://api.evmquery.com/api/v1/query/describe \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chain": "evm_ethereum",
"schema": {
"contracts": { "target": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }
},
"include": ["resolution"]
}'

The response lists every method evmquery resolved for that address, each carrying a real ABI fragment (name, inputs, outputs, stateMutability) and, for proxies, the executesAt address its logic actually runs on.

Decode against it

import { decodeAbiParameters, toFunctionSelector } from "viem";
const CALLDATA =
"0x70a0823100000000000000000000000028c6c06298d514db089934071355e5743bf21d60";
const resp = await fetch("https://api.evmquery.com/api/v1/query/describe", {
method: "POST",
headers: {
"x-api-key": process.env.EVMQUERY_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
chain: "evm_ethereum",
schema: {
contracts: { target: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } },
},
include: ["resolution"],
}),
});
const { contracts } = await resp.json();
const selector = CALLDATA.slice(0, 10);
const match = contracts[0].methods.find(
(m) => toFunctionSelector(m.abi) === selector,
);
if (!match) throw new Error(`no resolved method with selector ${selector}`);
const args = decodeAbiParameters(match.abi.inputs, `0x${CALLDATA.slice(10)}`);
console.log(match.abi.name, args);
// balanceOf [ '0x28C6c06298d514Db089934071355E5743bf21d60' ]

toFunctionSelector computes each resolved method’s selector locally; the one that matches the calldata’s first four bytes is the function that was called. Everything after that is a normal decodeAbiParameters call against that method’s inputs, no different from decoding against a hand-written ABI, except the ABI itself came from the address instead of a file you had to go find. The Calldata Decoder page has the equivalent Python version using eth_abi and eth_utils if that’s your stack.

Everything above is about calldata, the input side of a call. The mirror-image problem shows up on the output side: you call a function and get back could not decode result data (value="0x") instead of a value. That’s a different failure with different causes (unresolved proxies, wrong chain, a function that doesn’t exist on the deployed contract), and fixing “could not decode result data (0x)” in Ethers and Viem walks through all four of them. If you’re debugging a transaction end to end, expect to need both: this post for what was sent, that one for what came back.

Next steps

Share

Decode calldata from just an address

evmquery resolves the ABI server-side and matches your calldata's selector against it. No ABI file to find first.