Application Protocol Interfaces (APIs) have become the de facto way we connect to web applications today. It is an interface that is used to distill proprietary code down to a simple URL endpoint that can be accessed publicly without compromising the internal workings of the application. Application Binary Interfaces (ABIs) serve a similar purpose, but for Solidity and Ethereum. ABIs are the standard way to interact with smart contracts deployed to the Ethereum Virtual Machine (EVM). Similar to the structures found with APIs, ABIs encode a smart contract in a structured schema that is used as a gateway for high-level language interactions while also being able to be decoded into bytecode that is executed by EVM. Without the ABI, we would have to resort to writing interactions in bytecode instead of human-readable, high-level language code. In the following sections, I will discuss the encoding process as well as common ways the ABI is used for interaction.
Encoding
ABI encoding is not included in the Ethereum protocol by default. It requires a compiler because the data payload in a transaction that is processed by an EVM comes in a sequence of bytes. EVM cannot interpret the high-level language code of Solidity, which means a compilation engine must be used to translate our code into executable bytecode.
Solidity has a command-line compiler, solc, to handle this encoding that can be installed as a standalone tool, but is commonly bundled with development tools like Hardhat or Truffle. While the primary use of the compiler is to encode our Solidity into a bytecode file (.bin) and an ABI file, it has other features to extend its usage like the linking of libraries, compilation optimization based on the intent of the contract, and gas usage estimation.
The ABI file that is generated at compilation is a JSON file that uses a JSON-input-output interface. It follows a nested structure that details each function, variable, and method. The code below is an example of what this structure looks like:
{
"_format": "hh-sol-artifact-1",
"contractName": "ERC721NFT_Simple",
"sourceName": "contracts/ERC721NFT_Simple.sol",
"abi": [
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
}]
},
...
}
Despite the example above being an abbreviated version of what you would expect to output (the file is relatively large), you can at least notice the schema I described prior to the code example. The properties included in the ABI encoded functions are,
type = Identifying if it is a constructor, named function, or unnamed function
name = Name of the function
inputs[{}] = Array of objects with parameter name, data type and components
outputs[{}] = Array of objects with similar objects to inputs, but generated from the return statement
stateMutability = Type of function state
payable = Boolean with whether function accepts Ether
constant = Boolean indicating if a function is pure or view
Events share a similar structure, but with less information. More information on the structures can be found in the documentation.
Formats for Interaction
Once an ABI file is generated, you now have a gateway to interact with Smart contracts using a high-level language. Using a JavaScript library like ethers.js, you can interact with a smart contract by simply passing the address associated with the smart contract deployed and the ABI code. The address helps to identify the right contract in case there are multiple copies of a contract on the network and then the ABI attaches the smart contract interface to an abstract with the ability to interact with the contract through a browser, script, or app.
The example on the ethers.js website gives you a great walkthrough of how simple the process is once you are set up. After defining the address through its attached ENS name, an array is set that contains string references to the functions and expected values types, which are then looked up within the ABI and attached to the Contract abstraction. From there you can call functions as a method to daiContract, like daiContract.name()
.
// You can also use an ENS name for the contract address
const daiAddress = "dai.tokens.ethers.eth";
// The ERC-20 Contract ABI, which is a common contract interface
// for tokens (this is the Human-Readable ABI format)
const daiAbi = [
// Some details about the token
"function name() view returns (string)",
"function symbol() view returns (string)",
// Get the account balance
"function balanceOf(address) view returns (uint)",
// Send some of your tokens to someone else
"function transfer(address to, uint amount)",
// An event triggered whenever anyone transfers to someone else
"event Transfer(address indexed from, address indexed to, uint amount)"
];
// The Contract object
const daiContract = new ethers.Contract(daiAddress, daiAbi, provider);
While ABIs might have looked intimidating on the surface, I hope you can see now that they are pretty manageable due to the structured schema that allows you to be able to traverse the code no matter how large the smart contract might be that is compiled. Compilers do the tough bytecode translations for us and allow us to focus on building user experiences that create value with our smart contracts.