Every time you interact with an Ethereum smart contract — whether you are transferring tokens, swapping on a DEX, or minting an NFT — your transaction contains a blob of hexadecimal data called ABI-encoded calldata. Understanding ABI encoding is essential for any Ethereum developer who wants to debug transactions, build integrations, or write low-level Solidity code.
What is ABI?
ABI stands for Application Binary Interface. In the context of Ethereum, the ABI defines how data is encoded and decoded when communicating with smart contracts. Think of it as a contract's API specification: it tells callers what functions are available, what parameters they accept, and what data they return.
An ABI is typically represented as a JSON array that describes all public and external functions, events, and errors in a contract:
[
{
"type": "function",
"name": "transfer",
"inputs": [
{ "name": "to", "type": "address" },
{ "name": "amount", "type": "uint256" }
],
"outputs": [
{ "name": "", "type": "bool" }
]
}
]This JSON is generated by the Solidity compiler and is used by libraries like ethers.js and web3.js to encode function calls into the raw bytes that the EVM understands.
How ABI Encoding Works
When you call a smart contract function, the transaction's input data (calldata) is structured as follows:
[4-byte function selector] [32-byte encoded parameter 1] [32-byte encoded parameter 2] ...Step 1: Function Selector
The function selector is the first 4 bytes of the Keccak256 hash of the function's canonical signature. The canonical signature includes the function name and parameter types, with no spaces.
Function: transfer(address,uint256)
Keccak256: 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
Selector: 0xa9059cbb (first 4 bytes)This compact representation lets the EVM quickly identify which function to execute without needing the full function name. You can compute function selectors using our Keccak256 Hash Generator.
Step 2: Parameter Encoding
After the selector, each parameter is encoded according to its Solidity type. The ABI specification divides types into two categories:
Static Types
Static types have a fixed size and are encoded directly in place, always padded to 32 bytes.
| Type | Size | Encoding Rule |
|---|---|---|
uint256 | 32 bytes | Left-padded with zeros |
int256 | 32 bytes | Two's complement, left-padded |
address | 20 bytes | Left-padded to 32 bytes |
bool | 1 byte | 0x01 for true, 0x00 for false, left-padded |
bytes32 | 32 bytes | Right-padded with zeros |
Dynamic Types
Dynamic types (string, bytes, uint256[], etc.) use indirect encoding with an offset pointer:
- In the parameter's position, write a 32-byte offset pointing to where the actual data begins.
- At that offset, write the length of the data followed by the data itself.
This system allows the decoder to know exactly where each parameter's data lives, regardless of how large the dynamic data is.
Complete Example
Let's encode a call to transfer(address,uint256) with these parameters:
- to:
0xAbCdEf0123456789AbCdEf0123456789AbCdEf01 - amount:
1000000000000000000(1 ETH in Wei)
Function selector:
a9059cbb
Parameter 1 (address, padded to 32 bytes):
000000000000000000000000abcdef0123456789abcdef0123456789abcdef01
Parameter 2 (uint256, 32 bytes):
0000000000000000000000000000000000000000000000000de0b6b3a7640000
Full calldata:
0xa9059cbb
000000000000000000000000abcdef0123456789abcdef0123456789abcdef01
0000000000000000000000000000000000000000000000000de0b6b3a7640000You can verify this encoding using our ABI Encoder/Decoder — enter the function signature and the parameters to see the encoded result.
Dynamic Type Encoding: A Deeper Look
Dynamic types are more complex. Let's encode a call to a function with a dynamic array:
function batchTransfer(address[] calldata recipients, uint256 amount)With recipients = [0xAAA..., 0xBBB...] and amount = 100:
Selector: (4 bytes)
Offset to recipients array: 0x0000...0040 (64 bytes from start of params)
amount (static, in-place): 0x0000...0064 (100 in hex)
--- At offset 64 ---
Array length: 0x0000...0002 (2 elements)
Element 1 (address): 0x0000...0AAA...
Element 2 (address): 0x0000...0BBB...The key insight is that static parameters are encoded in-place, while dynamic parameters leave an offset pointer and store the actual data at the end of the parameter block.
ABI Encoding in Practice
Using ethers.js
The most common way to work with ABI encoding in JavaScript is through ethers.js:
import { AbiCoder, keccak256, toUtf8Bytes } from 'ethers';
const coder = AbiCoder.defaultAbiCoder();
// Encode parameters
const encoded = coder.encode(
['address', 'uint256'],
['0xAbCdEf0123456789AbCdEf0123456789AbCdEf01', '1000000000000000000']
);
// Decode parameters
const decoded = coder.decode(
['address', 'uint256'],
encoded
);
console.log(decoded[0]); // 0xAbCdEf0123456789AbCdEf0123456789AbCdEf01
console.log(decoded[1]); // 1000000000000000000n (BigInt)Using Solidity
Solidity provides built-in functions for ABI encoding:
// Standard encoding (each param padded to 32 bytes)
bytes memory data = abi.encode(addr, amount);
// Packed encoding (no padding, tightly packed)
bytes memory packed = abi.encodePacked(addr, amount);
// Encode a function call (selector + params)
bytes memory callData = abi.encodeWithSignature(
"transfer(address,uint256)",
recipient,
amount
);
// Encode with a known selector
bytes memory callData = abi.encodeWithSelector(
IERC20.transfer.selector,
recipient,
amount
);
// Decode
(address to, uint256 amt) = abi.decode(data, (address, uint256));abi.encode vs abi.encodePacked
This is a frequent source of confusion. The two functions produce very different outputs:
abi.encode(uint8(1), uint8(2))
// 0x0000...0001 0000...0002
// (64 bytes: each value padded to 32 bytes)
abi.encodePacked(uint8(1), uint8(2))
// 0x0102
// (2 bytes: values concatenated without padding)When to use which:
| Use Case | Function | Why |
|---|---|---|
| Calling contract functions | abi.encode | Standard ABI format required |
| Hashing for signatures | abi.encode | Avoids collision vulnerabilities |
| Merkle tree leaves | abi.encodePacked | More gas efficient, but use carefully |
| Data storage | abi.encode | Easier to decode later |
Reading ABI-Encoded Data on Etherscan
When you look at a transaction on Etherscan and click "Decode Input Data," Etherscan is performing ABI decoding. If the contract's ABI is verified, Etherscan can show you human-readable parameter names and values.
But what if the contract is not verified? You can still decode the calldata manually:
- Take the first 4 bytes — that is the function selector
- Look up the selector in a database like 4byte.directory
- Once you know the function signature, decode the remaining bytes according to the parameter types
Or simply paste the encoded data into our ABI Encoder/Decoder and let it handle the decoding for you.
Common ABI Encoding Patterns
ERC-20 Token Transfers
The most common ABI-encoded transaction on Ethereum:
Selector: 0xa9059cbb (transfer)
Params: [address recipient] [uint256 amount]ERC-20 Approvals
Selector: 0x095ea7b3 (approve)
Params: [address spender] [uint256 amount]Uniswap V2 Swaps
Selector: 0x38ed1739 (swapExactTokensForTokens)
Params: [uint256 amountIn] [uint256 amountOutMin] [address[] path]
[address to] [uint256 deadline]Multicall
Many DeFi protocols use multicall to batch multiple function calls into a single transaction:
Selector: 0xac9650d8 (multicall)
Params: [bytes[] data] // Array of ABI-encoded function callsTuple Encoding
Solidity structs are encoded as tuples in the ABI. A struct is treated as a sequence of its member types:
struct Order {
address maker;
uint256 amount;
uint256 price;
}
// ABI type for Order: (address,uint256,uint256)
// Encoded as: [address padded] [uint256] [uint256]Nested structs create nested tuples:
struct Trade {
Order buyOrder;
Order sellOrder;
uint256 timestamp;
}
// ABI type: ((address,uint256,uint256),(address,uint256,uint256),uint256)Error Encoding
Since Solidity 0.8.4, custom errors are also ABI-encoded using the same selector mechanism:
error InsufficientBalance(uint256 available, uint256 required);
// Selector: first 4 bytes of keccak256("InsufficientBalance(uint256,uint256)")
// Encoded: [selector] [available] [required]When a transaction reverts with a custom error, the revert data contains the ABI-encoded error, which tools like ethers.js can decode to give you a meaningful error message.
Frequently Asked Questions
What happens if I use the wrong ABI to decode data?
You will get garbage values. The decoder will interpret the bytes according to the types you specified, which will produce incorrect results if the types do not match the actual encoding. Always use the correct ABI for the contract you are interacting with.
Can I ABI-encode data without knowing the full ABI?
Yes, if you know the function signature (name and parameter types), you can encode and decode data. You do not need the full contract ABI — just the types of the parameters you are working with.
Why is ABI encoding 32-byte aligned?
The EVM operates on 32-byte (256-bit) words. By aligning all data to 32 bytes, the ABI encoding matches the EVM's native word size, making it efficient to read and write data in the virtual machine.
Is ABI encoding the same across all EVM chains?
Yes. ABI encoding is part of the EVM specification and is identical on Ethereum, Polygon, Arbitrum, Optimism, BSC, and all other EVM-compatible chains.
Try It Yourself
Want to encode or decode ABI data? Use our free ABI Encoder/Decoder to encode function calls or decode raw calldata directly in your browser.
Related Tools
- ABI Encoder/Decoder — Encode and decode smart contract calldata online
- Keccak256 Hash Generator — Generate function selectors and hash data
- Hex/Decimal Converter — Convert between hex and decimal values