ReCaseText
9 min read

ABI Encoding and Decoding: How Smart Contracts Communicate

The Application Binary Interface is the translation layer between human-readable Solidity code and the raw bytes the EVM actually executes. Here's how ABI encoding works, why it matters, and how to read raw calldata.

If you have ever submitted a transaction on Ethereum and looked at the "Input Data" field on Etherscan, you saw a long string of hexadecimal characters that looked nothing like the Solidity function you thought you were calling. That hex string is ABI-encoded calldata — the actual bytes the Ethereum Virtual Machine reads to figure out which function to run and what arguments to pass it. Understanding ABI encoding is essential for anyone who writes, audits, debugs, or interacts with smart contracts.

This article explains what the ABI is, how encoding and decoding work at the byte level, and how tools like our ABI encoder and ABI decoder help you translate between human-readable function calls and raw blockchain data.

What Is the ABI?

The Application Binary Interface — ABI for short — is a specification that defines how data structures and function calls are encoded into raw bytes for the Ethereum Virtual Machine. Think of it as a contract between the outside world and a smart contract deployed on-chain. The smart contract's compiled bytecode doesn't know anything about function names, parameter labels, or Solidity types. It only understands bytes. The ABI is the schema that tells both the caller and the contract how to structure those bytes so they agree on what's being communicated.

The ABI specification is maintained as part of the Solidity documentation, but it isn't Solidity-specific. Any language that compiles to EVM bytecode — Vyper, Yul, Huff, Fe — uses the same ABI encoding rules. And any client that sends a transaction to an Ethereum contract — whether it's a web3.js frontend, an ethers.js script, a Foundry test, or a raw JSON-RPC call — must ABI-encode its calldata according to the same specification.

The Function Selector

Every ABI-encoded function call begins with a four-byte function selector. This selector tells the EVM which function in the contract to execute. It is calculated by taking the Keccak-256 hash of the function's canonical signature and keeping only the first four bytes.

The canonical signature is the function name followed by a parenthesized, comma-separated list of parameter types — with no spaces and no parameter names. For a function declared in Solidity as transfer(address to, uint256 amount), the canonical signature is transfer(address,uint256). You hash that string with Keccak-256 using a tool like our Keccak-256 hash generator, and the first four bytes of the resulting hash become the selector.

For transfer(address,uint256), the Keccak-256 hash starts with a9059cbb, so every ERC-20 transfer call on every Ethereum contract begins with those four bytes. If you see 0xa9059cbb at the start of calldata on Etherscan, you immediately know the transaction is calling the transfer function.

The return type of a function is not part of its signature, and overloaded functions that differ only by return type would produce the same selector — which is why Solidity does not allow overloading based on return type alone.

How Arguments Are Encoded

After the four-byte selector, the remaining bytes encode the function's arguments. The ABI specification divides types into two categories: static types and dynamic types, and the encoding rules differ for each.

Static types have a fixed size known at compile time. These include uint256, int128, address, bool, bytes32, and fixed-size arrays like uint256[3]. Every static value is padded to exactly 32 bytes, regardless of its actual size. A bool value of true is encoded as 31 zero bytes followed by 0x01. An address (20 bytes) is left-padded with 12 zero bytes. A uint8 with a value of 255 is encoded as 31 zero bytes followed by 0xff. This consistent 32-byte slot size simplifies the EVM's memory model at the cost of space efficiency.

Dynamic types include bytes (arbitrary-length byte arrays), string, and dynamically-sized arrays like uint256[]. These cannot be placed inline because their length is not known in advance. Instead, the ABI uses an offset-pointer system. In the argument area, each dynamic type gets a 32-byte slot that contains an offset — the number of bytes from the start of the argument area to where the actual data begins. The actual data is then placed in a "tail" section after all the static values and offset pointers. The data region begins with a 32-byte length field followed by the content, padded to the nearest 32-byte boundary.

This design means you can always read static arguments at fixed offsets (first argument at byte 0, second at byte 32, third at byte 64, and so on), while dynamic arguments require a two-step lookup: read the offset, then jump to that position to read the length and data.

A Practical Encoding Example

Consider a function with the signature register(string,uint256,bool) called with the arguments "Alice", 42, and true. The encoding proceeds as follows.

The first four bytes are the function selector — the first four bytes of the Keccak-256 hash of register(string,uint256,bool).

Next come three 32-byte slots for the three arguments. The first argument is a string (dynamic), so its slot contains an offset pointer. The second argument is uint256 (static), so its slot contains the value 42 left-padded to 32 bytes. The third argument is bool (static), so its slot contains 1 left-padded to 32 bytes.

The offset for the string points past all three argument slots — to byte 96 (0x60) from the start of the argument area. At that position, the encoder writes the length of the string (5, for "Alice") as a 32-byte value, followed by the UTF-8 bytes of "Alice" right-padded to 32 bytes.

The result is a clean, predictable byte sequence that any ABI-compatible decoder can reverse without ambiguity — provided it has the function signature or the contract's ABI JSON.

The ABI JSON

When you compile a Solidity contract, the compiler outputs an ABI JSON file alongside the bytecode. This JSON describes every public and external function, event, and error in the contract — their names, parameter types, and whether each parameter is indexed (for events). Tools like Etherscan, web3.js, ethers.js, and our ABI decoder use this JSON to decode raw calldata back into human-readable function calls.

Without the ABI JSON, you can still decode static types by counting 32-byte slots, but you cannot know which function was called (the selector is just four bytes — you need to match it against a known signature) or where dynamic data boundaries fall. This is why verified contracts on Etherscan show decoded function calls while unverified contracts show only raw hex.

Signature databases like 4byte.directory and Openchain collect known function selectors, allowing partial decoding even without the full ABI JSON. If the selector 0xa9059cbb appears, the database can tell you it's transfer(address,uint256) and decode the arguments accordingly.

Static vs. Dynamic: Why It Matters

The distinction between static and dynamic encoding has practical implications for gas costs and contract design.

Static encoding is cheaper to decode on-chain because the EVM can calculate the exact memory offset of any argument with simple arithmetic — no pointer chasing required. Functions that accept only static types have predictable gas costs for calldata decoding.

Dynamic encoding adds overhead: each dynamic argument requires an extra 32-byte offset pointer, a 32-byte length field, and padding to the nearest 32-byte boundary. A function that accepts a string and a uint256[] will always cost more gas to decode than one that accepts two bytes32 values, even if the actual data is short.

This is one reason why experienced Solidity developers prefer bytes32 over string when the data fits in 32 bytes, and fixed-size arrays over dynamic arrays when the size is known. The ABI encoding overhead directly translates to gas costs on every call.

Tuples and Nested Structures

Solidity structs are encoded as tuples — ordered sequences of their component types. A struct with a uint256, an address, and a string is encoded identically to a function that takes those three types as separate arguments. The tuple is not wrapped in any additional container; its components are simply encoded in order according to the same static/dynamic rules.

Nested tuples (structs within structs) and arrays of tuples follow the same recursive pattern. Each nested dynamic type introduces another level of offset indirection, which is why deeply nested structures produce increasingly long and complex calldata.

Events and Error Encoding

The ABI specification also governs how events and custom errors are encoded. Event topics use the same Keccak-256 function selector mechanism — the first topic of a non-anonymous event is the hash of the event signature. Indexed event parameters are stored as separate topics (hashed if they're dynamic types), while non-indexed parameters are ABI-encoded in the event's data field.

Custom errors (introduced in Solidity 0.8.4) are encoded exactly like function calls: a four-byte selector derived from the error signature, followed by ABI-encoded arguments. When a contract reverts with a custom error, the revert data is ABI-encoded and can be decoded with the same tools you use for calldata.

Common Encoding Pitfalls

Several issues frequently trip up developers working with ABI encoding.

Selector collisions are theoretically possible because the selector is only four bytes, giving roughly 4.3 billion possible values. In practice, collisions are rare for normal function names, but the Solidity compiler checks for collisions within a single contract and will refuse to compile if two functions produce the same selector.

Off-by-one errors with offsets happen when manually constructing calldata. Each offset is relative to the start of the argument encoding area (after the four-byte selector), not relative to the start of the entire calldata. Miscounting by four bytes is a common mistake.

Encoding address vs. uint160 is functionally identical at the byte level, but tools may display them differently. An ABI decoder that expects an address will format the value with a 0x prefix and — if it supports EIP-55 — with a mixed-case checksum. The same bytes decoded as uint160 appear as a plain integer.

String encoding assumptions can cause problems. The ABI encodes strings as raw UTF-8 bytes with no null terminator. If a contract expects a specific string format (like a JSON blob or a URL), the encoding doesn't validate the content — it just packs the bytes.

Tools for ABI Encoding and Decoding

Our ABI encoder lets you specify a function signature and argument values, then produces the complete hex-encoded calldata. The ABI decoder does the reverse — paste in raw calldata and a function signature, and it extracts the individual argument values.

For computing function selectors manually, use our Keccak-256 hash generator — hash the canonical function signature, then take the first four bytes. And for converting between hex and decimal representations of encoded values, our hex-decimal converter handles the translation.

Understanding ABI encoding also connects to broader encoding concepts covered elsewhere on this site. If you're interested in how text becomes bytes in general, see our article on text to binary, ASCII, and Morse encoding. For Base64 encoding — another common way to represent binary data as text — see Base64 explained.

The Bottom Line

The ABI is the Rosetta Stone between human-readable smart contract code and the raw bytes the EVM executes. Every transaction, every event emission, every error revert on Ethereum follows the same encoding rules: a four-byte Keccak-256 selector followed by 32-byte-padded argument slots, with dynamic types using offset pointers to a trailing data section. Once you understand this structure, raw calldata on Etherscan stops being an opaque hex blob and becomes something you can read, verify, and debug. Our ABI encoder and ABI decoder make that translation instant, but knowing what's happening underneath gives you a real advantage when things go wrong.

References

Solidity ABI Specification — The canonical specification for ABI encoding, maintained as part of the Solidity documentation.

RareSkills — Understanding ABI Encoding for Function Calls — A detailed walkthrough of encoding and decoding with visual examples.

Decipher Club — Deep Mental Models for Solidity ABI Encoding — Part one of a deep dive into the encoding scheme with byte-level diagrams.

Ethereum Stack Exchange — What Is an ABI? — Community answers explaining ABI basics for beginners.