ABI Encoding Deep Dive: How Solidity Turns Your Data into Bytes
I’m currently open to collaborations and development projects across blockchain, smart contracts, and full-stack systems, feel free to connect if you’re building something interesting
When you call a function like
store({ number: 123, owner: “bob” })in Solidity, your wallet doesn’t send “123” or “bob” to the blockchain.
Instead, it sends a long string of hex bytes that the Ethereum Virtual Machine must decode back into those values.That process is defined by the Ethereum Application Binary Interface (ABI), a specification that describes how data structures and function calls are encoded and decoded.
In this post, we’ll do a deep dive into ABI encoding by manually breaking down a function call with a custom struct. You’ll see exactly how Solidity turns your inputs into bytes and how to decode them back again.
ABI Encoding Deep Dive
In Ethereum, the Application Binary Interface (ABI) defines how data structures and functions are encoded and decoded when interacting with smart contracts. Whether you’re calling a function manually, analyzing transaction calldata, or working with low-level call, understanding ABI encoding is essential.
Let’s explore ABI encoding using a slightly more complex example than simple uint256 functions: a contract with a custom struct.
pragma solidity 0.8.12;
contract Storage {
struct my_storage_struct {
uint256 number;
string owner;
}
my_storage_struct my_storage;
function store(my_storage_struct calldata new_storage) public {
my_storage = new_storage;
}
function retrieve() public view returns (my_storage_struct memory){
return my_storage;
}
}The output ABI:
[
{
“inputs”: [
{
“components”: [
{
“internalType”: “uint256”,
“name”: “number”,
“type”: “uint256”
},
{
“internalType”: “string”,
“name”: “owner”,
“type”: “string”
}
],
“internalType”: “struct Storage.my_storage_struct”,
“name”: “new_storage”,
“type”: “tuple”
}
],
“name”: “store”,
“outputs”: [],
“stateMutability”: “nonpayable”,
“type”: “function”
},
{
“inputs”: [],
“name”: “retrieve”,
“outputs”: [
{
“components”: [
{
“internalType”: “uint256”,
“name”: “number”,
“type”: “uint256”
},
{
“internalType”: “string”,
“name”: “owner”,
“type”: “string”
}
],
“internalType”: “struct Storage.my_storage_struct”,
“name”: “”,
“type”: “tuple”
}
],
“stateMutability”: “view”,
“type”: “function”
}
]Calling store(...)
When calling store({ number: 123456789, owner: “bob”}), the ABI encoding process works like this:
Function Selector (4 bytes):
It’s the first 4 bytes of the Keccak-256 hash of the function signature.
Signature:“store((uint256,string))”
Hash:0xddd456b3...
Selector:0xddd456b3
keccak256(”store((uint256,string))”) = 0xddd456b3Arguments Encoding (tuple):
Since we’re passing a struct with fields (uint256 number,string), ABI encodes the value as a padded 32-byte word:
2.1. Encoding of number:0x00000000000000000000000000000000000000000000000000000000075bcd15
2.2. Encoding of string “bob”:
Strings are dynamically sized in ABI encoding. They are encoded as a pointer to offset, followed by the string length and UTF-8 bytes.
Offset: relative to the start of the arguments block.
0x40(64 in decimal), because the string starts after 2 words (32 bytes * 2).0x0000000000000000000000000000000000000000000000000000000000000040Length of “bob”: 3
0x0000000000000000000000000000000000000000000000000000000000000003Bytes:
0x626f62(ASCII for“bob”), padded to 32 bytes0x626f620000000000000000000000000000000000000000000000000000000000
3. Final calldata:
0xddd356b3
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000075bcd15
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000003
626f620000000000000000000000000000000000000000000000000000000000Calling retrieve()
When calling function retrieve() public view returns (my_storage_struct memory) You’ll receive a return value that is ABI-encoded as a tuple:(uint256 number, string owner)
The decode from smart contract perspective would look:
(uint256 number, string memory owner) = abi.decode(returnData, (uint256, string));
// you will get
number = 123456789
owner = “bob”Summary
When you call a Solidity function, everything you send to the EVM is converted into bytes following the ABI specification.
The first 4 bytes of calldata are the function selector, computed as
keccak256(functionSignature)[0:4].Static types (like
uint256,address,bool) are encoded directly in 32-byte words.Dynamic types (like
string,bytes, arrays) are encoded as an offset pointer to their actual data, which appears later in calldata.Structs (tuples) combine these rules: each field is encoded in sequence, respecting static vs. dynamic layout.
Return values follow the same rules, just in reverse when decoding from
returnData.
In short, ABI encoding is what turns human-readable Solidity calls into the structured hex data that the EVM can process.
Once you understand it, you can read raw calldata, decode traces, and even craft function calls manually.

