Skip to main content

Type a word from a question or an article title.

3 · Under the hood

ABI: how a function call turns into bytes - selectors, calldata and events

How smart-contract calls are encoded - the function selector, argument alignment, dynamic types, custom errors and event topics, with real examples.

5 min readUpdated:
In this article

A contract is bytes, and it has to be called with bytes. The ABI (Application Binary Interface) is the agreement on how to turn "call the function transfer with such-and-such arguments" into a byte sequence both the interface and the contract understand. Knowing the ABI turns the mysterious string 0xa9059cbb000… into a readable call.

The function selector

A contract receives one block of bytes as input (calldata). How does it know which function was called? By the first four bytes - the selector.

The selector is the first 4 bytes of the keccak256 hash of the function signature: the name and argument types without spaces.

text
signature:  transfer(address,uint256)
keccak256:  a9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
selector:   0xa9059cbb

This is one of the best-known selectors: every ERC-20 token transfer begins with it. Argument names do not take part in the signature - only types.

The contract starts execution by comparing the selector with a list of known functions and jumping to the right one. Here are a few selectors of our escrow:

FunctionSelector
confirmDelivery(uint256)0xfd84cb97
acceptOffer(uint256,bytes32)0x72dfe66b
createOffer((bool,address,address,uint16,uint16,address,uint256,uint8,bytes32))0xd96ef60e

Notice that the createOffer call takes a structure - in the signature it is written as a tuple in parentheses.

Encoding arguments

After the selector come the arguments. The rule for simple types: each takes 32 bytes, that is it is aligned to a word.

The call transfer(0x6462…90aD, 1000000) is 4 + 32 + 32 = 68 bytes:

text
0xa9059cbb                                                         // selector
0000000000000000000000006462dddc0a3d893a7c17f94410e2d27162a390ad   // address, left-padded with zeros
00000000000000000000000000000000000000000000000000000000000f4240   // 1000000 in hexadecimal

An address is 20 bytes, so it is left-padded with 12 zero bytes. A number is left-padded with zeros because it is stored as a big-endian uint256. See f4240? That is 1,000,000: for USDT with 6 decimals - exactly one token.

Structures and tuples

A structure consisting of static types (numbers, addresses, bool, bytes32) is encoded "inline": fields follow one another, 32 bytes each. The createOffer parameters are nine fields, so 9 × 32 = 288 bytes after the selector:

text
asBuyer, counterparty, arbiter, arbiterBaseFeeBps, arbiterDisputeFeeBps,
token, amount, durationPreset, termsHash

Each field is padded to a word: a bool is 32 bytes whose last byte is 0 or 1; a uint16 with the value 100 is 31 zero bytes and the byte 0x64.

Dynamic types

Strings, bytes and arrays have variable length, so they are encoded differently: the "head" stores an offset to the data, and the data itself lies in the "tail": first the length, then the contents padded to a word boundary.

For a function f(uint256 a, string s) the call looks like this: the selector, then a, then the offset to s (0x40 - right after the two head words), and then in the tail the string's length and its bytes. Decoding that by hand is inconvenient, so there are libraries (viem, ethers) that take the ABI as JSON and do it all themselves.

The ABI file

The ABI as JSON is a description of all of a contract's functions, events and errors: names, types, inputs, outputs, mutability (view, payable). It lets an interface turn confirmDelivery(7) into the right bytes and the replies back into numbers. If the ABI does not match the contract, a call either fails or does the wrong thing. So in the project the ABI is generated from the compiled contract and not written by hand.

Errors

Custom errors (error TokenNotAllowed(address token);) are encoded too: 4 bytes of error selector plus arguments. For TokenNotAllowed(address) that is 0x51c9316d. When a transaction reverts the node returns these bytes, and the interface finds the human text by the selector: "This currency is not allowed on the network".

This is how an interface turns a technical revert into a clear hint rather than "execution reverted".

Events and topics

An event is written to the transaction journal as a log. Its structure: topic0 is the keccak256 of the event signature; the following topics are the values of parameters marked indexed; the rest goes into the data field.

For our deal-completion event:

text
DealSettled(uint256 indexed dealId, uint8 indexed outcome,
            uint256 buyerAmount, uint256 sellerAmount,
            uint256 arbiterFee, uint256 platformFee)

topic0 = 0xd716fe621ea710daf4a2c1145a1bc9d712441578dcb8c686ed242ea2590e7789
topic1 = dealId           (indexed)
topic2 = outcome          (indexed)
data   = buyerAmount ‖ sellerAmount ‖ arbiterFee ‖ platformFee

Indexed parameters can be searched: "show all DealSettled events for deal 7" is a topic query. Non-indexed data cannot be searched but is cheaper. A developer thinks this trade-off through in advance: they index what will be searched. How this is used to build a deal's history is in the article on events and logs.

Tools for checking

  • cast sig "transfer(address,uint256)" - a selector from a signature (a Foundry tool).
  • cast 4byte 0xa9059cbb - look up a name by selector in a public database.
  • Decode Input Data in the explorer - a ready decoding.
  • viem in TypeScript: encodeFunctionData, decodeFunctionData, toFunctionSelector.

Why this matters for security too

Selectors are 4 bytes, so collisions are possible: different functions with the same selector. For an ordinary contract this is not a problem (the compiler will tell you), but in proxy architectures such matches have been used in attacks. Another takeaway is simpler: when you sign a transaction in the wallet you sign these very bytes. If the wallet can decode the ABI - read the decoding; if it cannot - that is a reason to be wary.

Having understood the bytes, let us move to an important practice: how to read accumulated events without losing money and nerves to public-node limits: events, logs and indexing.

Mentioned in

Ready to try it?

Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.