introduction
ABI encoding is the standard way used in the Ethereum ecosystem to communicate and exchange data with smart contracts. It can also be defined as a process in Solidity that converts function signatures, arguments, and other data types into their binary format, ensuring that data can be correctly transmitted between external callers (web applications) or even between contract-to-contract communication in the blockchain. This encoding is essential for function invocations and decoding of returned data from functions.
Encoding
The solidity programming language offers diverse functions for encoding and decoding data and in this article, we shall have a look at a few of them like, abi.encode
, abi.encodePacked
, abi.encodeWithSelector.
Abi encode
abi.encode
is a Solidity function that encodes one or more input values into their binary (byte array) format based on the Ethereum ABI standard. This encoding follows strict rules, ensuring compatibility when passing data between contracts or interacting with external callers. The resulting binary data can be used for low-level contract calls, stored, or transmitted, and it retains all the type information needed for decoding back to the original values.
function encode(string memory text) public pure returns (bytes memory) {
return abi.encode(text);
}
Abi encoded packed
The function abi.encodePacked
is also used to convert any given input into its binary format, unlike its sibling abi.encode
, abi.encodePacked
follows a different approach when it comes to data encoding. Here the function encodes the data into tightly packed binary format, unlike its sibling that uses padding and preserving type information for decoding, this approach is best for storage optimization due to the short size of the encoded data, but this advantage comes with few tradeoffs when decoding, and one of the most famous tradeoffs here is collision resistance when performing hashing operations since two input can produce the same output, for example, AA + BBA encode with abi.encodePacked
will result in AABBA at the decoding stage, but if you try to encode A + ABBA you will get the same output as on the previous operation, you can find the code bellow.
abi.encodePacked
function encodePacked(string memory text)
public
pure
returns (bytes memory)
{
return abi.encodePacked(text);
}
This example shows how working abi.encodePacked
can later result in
collision issues when performing hashing
function willCreatecollision() public pure returns (bool) {
return
keccak256(abi.encodePacked("AA", "ABB")) ==
keccak256(abi.encodePacked("A", "AABB")); //true
}
Function Signatures and Function Selectors
Before diving into function signatures and selectors, it's essential to understand what an Application Binary Interface (ABI) is and why it's crucial in the Ethereum ecosystem. The ABI serves as a scheme used in Ethereum for interacting with smart contracts, acting as a bridge between the human-readable code and the bytecode executed by the Ethereum Virtual Machine (EVM).
Think of an ABI as a dictionary that defines the structure of a given smart contract. When building smart contracts, developers typically use high-level languages like Solidity or any compatible EVM language. This human-readable code is then compiled into bytecode, which the EVM can interpret. Importantly, this means that the written code never directly reaches the Ethereum network. Unlike traditional backend systems, where you can call functions via API endpoints in the same way they are written, smart contracts operate differently. To execute a specific function from your smart contract, you must identify the exact function to invoke in the compiled bytecode.
Function Signature
A function signature comprises the function's name and the types of parameters it accepts, concatenated as a string without spaces. example. transfer(address,uint256)
Function Selectors
A function is selected from the bytecode using a function selector,
which is the first 4 bytes of the Keccak-256 hash of the function signature because these are used to uniquely identify functions within a contract
In the example below we create a function selector for the transfer function, the selector will later be used for calling the transfer function.
function transfer(address recipient, uint256 amount)
public
pure
returns (string memory)
{
//Simple empty function
return "You called the transfer function";
}
function createFunctionSelector() public pure returns (bytes4) {
return
bytes4(
keccak256(
abi.encodePacked(
"transfer(address,uint256)"
)
)
); //create function signature and take the first four bytes.
}
After creating the function selector we can add all the necessary parameters to call the transfer function.
function addDataTobeCalledWithTheTransferFunction(
address recipient,
uint256 amount
) public pure returns (bytes memory) {
return
abi.encodeWithSelector(createFunctionSelector(), recipient, amount);
}
With all that information, we can proudly call the transfer function
function callTransferFunctionDirectly(address recipient, uint256 amount)
public
returns (bool, string memory)
{
(bool success, bytes memory returnedDate) = address(this).call(
(addDataTobeCalledWithTheTransferFunction(recipient,amount))
);
require(success, "Call failed");
return (success, abi.decode(returnedDate,(string)));
}
Beyond the Blockchain
After learning all these concepts, you must ask yourself who we can communicate with smart contracts if we have to perform all those hashing and data serialization. Hashing functions like Keccak256 can be used beyond the EVM and in the example below I demonstrate how to create function signatures, and how to derive a function selector from them. I will be using node.js with the Keccak256 node.js library.
const keccak256 = require("keccak256");
const signature = "transfer(address,uint256)";
const address = "0x19708648D2f76607B32CcB1240B0DfC67a222a75";
const amount = 2;
//This will create the same signature as the one we saw on Remix
const generateFunctionSelector = () => {
const hash = keccak256(signature).toString("hex");
return "0x" + hash.toString().slice(0, 8);
};
//The returned information can be used directly when making calls to the EVM
function addParameters() {
const serializeAddress = address
.toLowerCase()
.replace("0x", "")
.padStart(64, 0);
const functionSelector = generateFunctionSelector();
const serializeAmount = amount.toString(16).padStart(64, "0");
const transactionData = "".concat(
functionSelector,
serializeAddress,
serializeAmount
);
return transactionData;
}
console.log(addParameters());
Ohhh😤! That was a lot to cover, thanks for reading the article. Let me know your thoughts in the comments.
All codes and examples can be found here. repo
Top comments (0)