As we learned in 🔩 Why Learn ABI Encoding?, ABI encoding is used everywhere in smart contract interaction and development. The primary use case is to encode function calls to send to smart contracts, expecting those smart contract to interpret that message and consequently run the code you want.
In this lesson, we will learn precisely what one of these function call messages might look like. There is binary involved, but it’s relatively simple when broken down.
Here’s a full example of an ABI-encoded function call:
0x71a6155e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000036162630000000000000000000000000000000000000000000000000000000000
In an EVM function call, this data becomes the calldata for a contract to decode and interpret as it sees fit.
Let’s learn how to pick it apart.
First off, the encoded message above intends to call this [silly, rudimentary] function:
interface Example {
function setGreeting(bool exclaim, string greeting, uint x) external;
}
(This is written as a Solidity interface only for convenience; ABI encoding is not Solidity-exclusive, although Solidity certainly uses it to handle function calls).
A contract (or any other code working with ABIs) must be aware of the interface of setGreeting()
in order to both encode and/or decode ABI messages for it. In other words, you can’t derive any types from the message itself; you must know the types beforehand. Keep this in mind throughout the lesson.
Now let’s dive a little deeper. An ABI-encoded function call has two main parts:
A function selector is a 4-byte “identifier” for specifying which function you want to call. We write “identifier” in quotes, because it’s not a unique identifier; there’s a chance for two functions to have the same function selector, and that chance is significant enough to be a security concern, especially when working with advanced smart contract patterns such as upgradability (but more on that later).
Here is a brief description of how a function selector is generated:
In JavaScript:
let ethers = require('ethers');
let bytestring = ethers.utils.toUtf8Bytes('setGreeting(bool,string,uint256)');
let hash = ethers.utils.keccak256(bytestring);
// 2 chars for the 0x prefix; 8 chars for the 4 bytes.
let selector = hash.substring(0, 2 + 8);
selector //=> "0x71a6155e"
Take a closer look at the string used in the code example. Note that:
uint256
instead of uint
)memory
for a string
parameter)Since we’re only taking the first four bytes of a hash, it’s easy to imagine many other hashes having the same first four bytes, hence the potential for a conflict.
On the other hand, function selectors will always be four bytes. This is beneficial because they are so common; every time you want to define a function or call a function in bytecode, you will be working with a function selector. Only needing 4 bytes vs a full 32 bytes can save a lot of bytecode size.
Now it’s time to dig into the algorithm for ABI-encoding function call arguments. Key things to note about this algorithm:
0x01
, you can't tell – based on the value alone – if it's a number, a boolean, an address, etc. This is why you must know the function’s interface to know what type you’re working with.Recall that we are working with this ABI-encoded function call (the example from above):
0x71a6155e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000036162630000000000000000000000000000000000000000000000000000000000
to a contract that claims to have this interface:
interface Example {
function setGreeting(bool exclaim, string greeting, uint x) external;
}
To pick this ABI-encoded message apart, the first thing we want to do is split it into three sections:
Splitting up our example into these sections, we get:
0x // Discarded; not part of the data sent, only a prefix for human eyes.
// 1. Function Selector
71a6155e
// 2. Parameters
0000000000000000000000000000000000000000000000000000000000000001 - bool
0000000000000000000000000000000000000000000000000000000000000060 - string offset
0000000000000000000000000000000000000000000000000000000000000001 - uint256
// 3. Complex Data
0000000000000000000000000000000000000000000000000000000000000003 - string length
6162630000000000000000000000000000000000000000000000000000000000 - string data
Several things to note:
true
and 1
respectively, both represented as the binary value 0x1
).0x60
is a byte offset to where the string data starts.0x60
is 96
in decimal. Thus the string data begins 96 bytes from the beginning of the Parameters section.For string data specifically:
0x3
floor(L / 32) + 1
words is the UTF-8 encoded string data itself. In this case, 0x616263
, which is the string "abc"
in UTF-8.Based on this analysis, combined with knowledge of the Example
/setGreeting
interface, we can deduce that this ABI-encoded function call is equivalent to the following function call written in Solidity:
Example(someAddress).setGreeting(true, "abc", 1);