The ABI and 4-Byte Function Selectors
The previous page showed how a high-level language compiles down to EVM bytecode. But a contract’s bytecode is one flat program with one entry point — the EVM just starts executing at byte 0. So how does a caller who wants transfer(...) end up running the transfer code and not the balanceOf code, when both live in the same blob?
The answer is a convention, not a protocol rule. The EVM itself knows nothing about “functions.” It hands the contract a raw byte string — the data field of the transaction, which we named calldata back in anatomy of a transaction — and starts running. Everything above that byte string, the whole idea that a contract has functions you can name and call, is a shared encoding both sides agree to. That encoding is the ABI, and its most visible fingerprint is the first four bytes of every call. This page builds both from first principles.
Why a bare contract has no functions
Section titled “Why a bare contract has no functions”Recall the mini-EVM counter from the build-your-own-EVM part. Its entire body was seven opcodes:
PUSH 0 ; key SLOAD ; stack: [ count ] PUSH 1 ADD ; stack: [ count + 1 ] PUSH 0 ; key SSTORE ; storage[0] = count + 1 STOPIt does exactly one thing, the same way, every time it is called. There is no “which function?” question because there is only one behavior. It never even looks at its calldata — the companion crate’s VM has no calldata concept at all:
// ethmini/src/contracts.rs — the whole counter, no input readpub fn counter_code() -> Vec<u8> { assemble(&[ Op::Push(COUNTER_SLOT), Op::SLoad, Op::Push(1), Op::Add, Op::Push(COUNTER_SLOT), Op::SStore, Op::Stop, ])}That is a contract with zero functions in the ABI sense. To get a contract that can transfer, approve, balanceOf, and totalSupply — many behaviors behind one address — you need two things the counter lacks:
- A way for the caller to say which behavior they want.
- Code at the top of the contract that reads that request and jumps to the matching behavior.
Neither is an EVM feature. Both are the ABI. The rest of this page is those two things.
The ABI: an agreed encoding for calls
Section titled “The ABI: an agreed encoding for calls”ABI stands for Application Binary Interface. It is the agreed-upon convention for turning a human idea — “call transfer with recipient 0xAbc… and amount 100” — into the flat bytes blob that goes in a transaction’s data field, and for turning that blob back into arguments the contract can use. It is “binary” because it operates on raw bytes, and an “interface” because it is the contract boundary that a caller and a contract both promise to honor.
Nothing enforces the ABI. A contract could interpret its calldata any way it likes. But if it wants ordinary wallets, block explorers, and other contracts to be able to call it, it follows the standard ABI that the Solidity/Vyper toolchains and the whole ecosystem assume. The ABI is a social contract layered on top of the machine — exactly the kind of shared convention that lets untrusting strangers interoperate without a central coordinator.
A call’s calldata has a rigid two-part shape:
calldata = [ 4-byte selector ][ ABI-encoded arguments ] └── which function ──┘└──── its inputs ────┘The first four bytes name the function. Everything after is the arguments, packed in a fixed layout. We take each part in turn.
The 4-byte function selector
Section titled “The 4-byte function selector”To name a function in four bytes, the ABI needs a deterministic way to turn a function description into a short, collision-resistant tag that both the caller and the contract compute the same way, offline, with no lookup table. Ethereum already has the perfect tool: its hash function, keccak256.
The function selector is defined as the first 4 bytes of keccak256 of the function’s canonical signature.
The canonical signature is the function’s name followed by its parameter types in parentheses, comma-separated, with no spaces, no argument names, and no return types. For an ERC-20 transfer:
canonical signature : "transfer(address,uint256)" keccak256(...) : 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b selector (first 4) : 0xa9059cbbThat 0xa9059cbb is one of the most-recognized byte strings in Ethereum — it prefixes essentially every token transfer ever made. You can reproduce it with a single line of any keccak library:
# using Foundry's cast; prints 0xa9059cbbcast sig "transfer(address,uint256)"
# the long way, hashing the string yourself and taking 4 bytescast keccak "transfer(address,uint256)" | cut -c1-10Four bytes is 2^32 ≈ 4.29 billion possible selectors. That is enormous relative to the handful of functions on any one contract, so within a single contract collisions are vanishingly unlikely to happen by accident — which is exactly what lets a contract use the selector as a reliable “which function” tag.
Under the hood — why canonical matters
Section titled “Under the hood — why canonical matters”The word canonical is load-bearing. The hash is computed over an exact byte string, so every character matters. All of these are different signatures and produce different selectors:
"transfer(address,uint256)" → 0xa9059cbb ✅ canonical "transfer(address, uint256)" → different! (a space) "transfer(address,uint)" → different! (uint is an alias, not canonical) "transfer(address,uint256 to)" → different! (argument name included)Two rules make the signature canonical, and both trip people up:
- Types are spelled out in full. Solidity lets you write
uintas shorthand foruint256, but the ABI signature must use the fully-expandeduint256. Hashuintand you get a selector no standard tool will ever call. - No whitespace, no names, no returns. The signature is
name(type1,type2,...)and nothing else. A stray space or an argument name changes the bytes and therefore the hash.
Get the canonical form wrong and the symptom is baffling: your call compiles and sends fine, but the contract behaves as if you called a function it doesn’t have (see dispatch, next), and typically reverts or silently hits a fallback. The bytes were valid; they just named a selector nobody was listening for.
Dispatch: how the contract picks a function
Section titled “Dispatch: how the contract picks a function”Now the receiving side. When a call arrives, the contract’s compiled bytecode begins with a small block of code — the dispatcher, sometimes called the function selector table or “the jump table.” Its job is pure and mechanical:
- Load the first 4 bytes of calldata (the incoming selector).
- Compare it, in turn, against each selector the contract knows about — its own functions’ selectors, which the compiler baked in as constants.
- On a match, jump to that function’s code.
- If nothing matches, run the fallback (or
receive) function if one exists, otherwise revert.
In pseudo-EVM it looks like a chain of “if selector == X, goto handler_X”:
; calldata: [ a9059cbb | <args...> ] load selector = calldata[0..4] ; = 0xa9059cbb
if selector == 0xa9059cbb goto transfer ; transfer(address,uint256) if selector == 0x70a08231 goto balanceOf ; balanceOf(address) if selector == 0x095ea7b3 goto approve ; approve(address,uint256) if selector == 0x18160ddd goto totalSupply ; totalSupply() ; ...no match... goto fallback_or_revertThat is the entire mechanism by which “one contract, many functions” works. It is nothing more than the counter’s single entry point, with a switch statement bolted to the front that reads four bytes and jumps. In Solidity you never write this dispatcher — the compiler generates it from your public/external functions. But it is there, at the top of the runtime bytecode, and it is the reason a call with the wrong selector goes nowhere.
// The compiler turns these declarations...contract Token { function transfer(address to, uint256 amount) external returns (bool) { /* ... */ } function balanceOf(address who) external view returns (uint256) { /* ... */ }}
// ...into a dispatcher that switches on keccak256("transfer(address,uint256)")[:4]// = 0xa9059cbb and keccak256("balanceOf(address)")[:4] = 0x70a08231.Encoding the arguments
Section titled “Encoding the arguments”The selector says which function; the bytes after it say with what. The ABI’s argument encoding has one dominant rule that explains almost everything about how the bytes look:
Everything is packed into 32-byte (256-bit) words.
The EVM’s native unit is the 256-bit word — its stack items, its storage slots, its arithmetic are all 256 bits wide. So the ABI aligns arguments to that width. A single uint256 is one word. A bool is one word (31 zero bytes and a 0x01 or 0x00). An address is 20 bytes, left-padded with 12 zero bytes to fill a word. Every “static” (fixed-size) argument becomes exactly one 32-byte word, laid down in order.
For transfer(0x00000000000000000000000000000000DeadBeef, 100) the full calldata is:
0xa9059cbb ← selector (4 bytes) 000000000000000000000000 00000000000000000000000000000000DeadBeef ← arg 1: address, left-padded to 32 bytes 0000000000000000000000000000000000000000000000000000000000000064 ← arg 2: uint256 100 = 0x64, right-alignedThat is 4 + 32 + 32 = 68 bytes total. The contract’s transfer handler knows its own signature, so it knows to read the first word as an address and the second as a uint256, in that fixed order — no field names or type tags travel on the wire, because the selector already pinned down the layout.
Dynamic types — bytes, string, arrays of variable length — don’t fit in a fixed word, so the ABI uses an extra level of indirection: the argument slot holds an offset (a pointer, in bytes, into the calldata) to where the real data lives later in the blob, and the data itself is prefixed with a length word. You rarely encode this by hand; the takeaway is the shape:
[selector][ head: one 32-byte slot per argument ][ tail: dynamic data ] static value, or offset ─┐ ▲ └────────────┘ points hereSelector collisions
Section titled “Selector collisions”Because a selector is only 4 bytes, two different signatures can hash to the same selector — a collision. Across all possible function names this is not just possible, it is easy to manufacture: with 2^32 selectors, you can grind function names until one matches a target selector in seconds. Databases of “4-byte” signatures already list many colliding pairs.
Within one honest contract, the compiler will refuse to build if two of your functions share a selector, so accidental collisions inside a contract are caught at compile time. The danger is at the boundaries:
- Proxy contracts. A proxy forwards calls to an implementation based on the incoming selector. If a function on the proxy itself shares a selector with a function on the implementation, calls meant for one can hit the other. This is the “function clash” problem, and it is a known proxy footgun.
- Interpreting unknown calldata. Any code that decides behavior purely from the first 4 bytes of attacker-supplied calldata can be fooled by a crafted signature that collides with a privileged selector.
The lesson is the same one the canonical rule taught: a selector is a tag, not a proof. It is a compact, deterministic label that is excellent at routing honest calls and terrible as a security boundary. Never trust a selector to establish identity — trust it only to route.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? The EVM offers one entry point and a flat byte string; the ABI exists to build the human abstraction of named functions with typed arguments on top of that raw substrate, using nothing but a hash and a convention both sides compute independently.
- What problem does it solve? It lets one contract expose many callable behaviors — and lets any wallet, explorer, or contract call them — without a central registry, by deriving a deterministic 4-byte tag from each function’s signature and packing arguments into a fixed, self-describing-by-position layout.
- What are the trade-offs? Four bytes is compact and lookup-free, but it is lossy: distinct signatures can collide, so a selector routes but cannot authenticate. The 32-byte word alignment is simple and matches the EVM, but it wastes calldata gas on padding.
- When should I avoid it? Never for normal contract calls — it is the calling convention. But do not lean on selectors for security (identity, access control) or on their uniqueness across untrusted code; and reach past the standard ABI’s padding only in extreme gas-golfing, where you take on the burden of custom decoding.
- What breaks if I remove it? Without the selector convention, a contract can’t tell one requested function from another — you are back to the counter: one address, one hardcoded behavior. Wallets and explorers lose the ability to construct or decode any call, and the entire “one contract, many functions” model — every token, every DEX, every DAO — collapses.
Check your understanding
Section titled “Check your understanding”- What does “ABI” stand for, and in one sentence, what does it do? Why is it a convention rather than something the EVM enforces?
- Write out, in order, how you compute the function selector for
approve(address,uint256). What are the two rules that make a signature canonical, and what goes wrong if you get the canonical form wrong? - Describe the dispatcher at the top of a compiled contract. What are its four possible outcomes for an incoming call, in order?
transfer(address,uint256)produces 68 bytes of calldata. Break down where those 68 bytes come from, and explain why theaddressargument is left-padded with 12 zero bytes.- Two different function signatures can produce the same 4-byte selector. Why is this possible, why can’t it happen by accident within a single contract, and what is the one-sentence security lesson for proxies?
Show answers
- Application Binary Interface — the agreed encoding for turning a function call and its typed arguments into the flat calldata byte blob, and back again. It is a convention because the EVM knows nothing of “functions”: it just hands the contract calldata and starts executing at byte 0. The ABI is a social/tooling standard layered on top so that wallets, explorers, and contracts can interoperate.
- Take the canonical signature
"approve(address,uint256)"(name + parenthesized parameter types), computekeccak256of that exact string, and take the first 4 bytes of the hash. Canonical means (a) types are fully spelled out —uint256, never theuintalias — and (b) no spaces, no argument names, no return types. Get it wrong and you compute a different selector: the call is well-formed but names a function the contract isn’t listening for, so it hits the fallback or reverts. - The dispatcher is a code block at the start of the runtime bytecode that loads the first 4 bytes of calldata and compares them against the contract’s known selectors. Its four outcomes, in order: (1) load the incoming selector; (2) match it against each known selector; (3) on a match, jump to that function’s code; (4) on no match, run the fallback/
receiveif one exists, else revert. 4bytes for the selector +32bytes for the first argument +32bytes for the second = 68 bytes. Every static argument is padded to a full 32-byte (256-bit) word because that is the EVM’s native word size. Anaddressis only 20 bytes, so it is left-padded with 12 zero bytes to fill the 32-byte word, keeping the value right-aligned like an integer.- A selector is only
2^32possible values, far smaller than the space of all signature strings, so distinct signatures can hash to the same first 4 bytes — and such collisions can even be deliberately ground out. It can’t happen accidentally within one contract because the compiler refuses to build if two of your own functions share a selector. Security lesson: 4 bytes route a call, they do not authorize it — never let a selector clash decide whether a call reaches an admin/privileged function (the reason for the transparent proxy pattern).