As a consequence of building the payload executor contract, I have become very familiar with encoding and decoding calldata.
I briefly mentioned the concept in the UniswapV2 Arbitrage Bot project. If you paid special attention to the functions execute_mempool_arb()
and execute_onchain_arb()
, you saw that I encoded some calldata directly using the eth_abi
library before building the call to the payload executor contract.
That project lesson was very dense so I glossed over it. This lesson will serve as a more thorough exploration of the topic.
ABI and Calldata
At a high level, the ABI (Application Binary Interface) is a means for programs to properly encode calldata (the byte-encoded arguments to smart contract functions). The ABI lists, among other things, a full description of the arguments for each function, including their data type and ordering.
Let’s take an example, using the familiar WETH contract. If you export the ABI from Etherscan and tidy it up, you’ll see the following:
[{'constant': True,
'inputs': [],
'name': 'name',
'outputs': [{'name': '', 'type': 'string'}],
'payable': False,
'stateMutability': 'view',
'type': 'function'},
{'constant': False,
'inputs': [{'name': 'guy', 'type': 'address'},
{'name': 'wad', 'type': 'uint256'}],
'name': 'approve',
'outputs': [{'name': '', 'type': 'bool'}],
'payable': False,
'stateMutability': 'nonpayable',
'type': 'function'},
{'constant': True,
'inputs': [],
'name': 'totalSupply',
'outputs': [{'name': '', 'type': 'uint256'}],
'payable': False,
'stateMutability': 'view',
'type': 'function'},
{'constant': False,
'inputs': [{'name': 'src', 'type': 'address'},
{'name': 'dst', 'type': 'address'},
{'name': 'wad', 'type': 'uint256'}],
'name': 'transferFrom',
'outputs': [{'name': '', 'type': 'bool'}],
'payable': False,
'stateMutability': 'nonpayable',
'type': 'function'},
{'constant': False,
'inputs': [{'name': 'wad', 'type': 'uint256'}],
'name': 'withdraw',
'outputs': [],
'payable': False,
'stateMutability': 'nonpayable',
'type': 'function'},
{'constant': True,
'inputs': [],
'name': 'decimals',
'outputs': [{'name': '', 'type': 'uint8'}],
'payable': False,
'stateMutability': 'view',
'type': 'function'},
{'constant': True,
'inputs': [{'name': '', 'type': 'address'}],
'name': 'balanceOf',
'outputs': [{'name': '', 'type': 'uint256'}],
'payable': False,
'stateMutability': 'view',
'type': 'function'},
{'constant': True,
'inputs': [],
'name': 'symbol',
'outputs': [{'name': '', 'type': 'string'}],
'payable': False,
'stateMutability': 'view',
'type': 'function'},
{'constant': False,
'inputs': [{'name': 'dst', 'type': 'address'},
{'name': 'wad', 'type': 'uint256'}],
'name': 'transfer',
'outputs': [{'name': '', 'type': 'bool'}],
'payable': False,
'stateMutability': 'nonpayable',
'type': 'function'},
{'constant': False,
'inputs': [],
'name': 'deposit',
'outputs': [],
'payable': True,
'stateMutability': 'payable',
'type': 'function'},
{'constant': True,
'inputs': [{'name': '', 'type': 'address'}, {'name': '', 'type': 'address'}],
'name': 'allowance',
'outputs': [{'name': '', 'type': 'uint256'}],
'payable': False,
'stateMutability': 'view',
'type': 'function'},
{'payable': True, 'stateMutability': 'payable', 'type': 'fallback'},
{'anonymous': False,
'inputs': [{'indexed': True, 'name': 'src', 'type': 'address'},
{'indexed': True, 'name': 'guy', 'type': 'address'},
{'indexed': False, 'name': 'wad', 'type': 'uint256'}],
'name': 'Approval',
'type': 'event'},
{'anonymous': False,
'inputs': [{'indexed': True, 'name': 'src', 'type': 'address'},
{'indexed': True, 'name': 'dst', 'type': 'address'},
{'indexed': False, 'name': 'wad', 'type': 'uint256'}],
'name': 'Transfer',
'type': 'event'},
{'anonymous': False,
'inputs': [{'indexed': True, 'name': 'dst', 'type': 'address'},
{'indexed': False, 'name': 'wad', 'type': 'uint256'}],
'name': 'Deposit',
'type': 'event'},
{'anonymous': False,
'inputs': [{'indexed': True, 'name': 'src', 'type': 'address'},
{'indexed': False, 'name': 'wad', 'type': 'uint256'}],
'name': 'Withdrawal',
'type': 'event'}]
There are two functions that we use often, deposit
and withdraw
.
Recall that deposit()
requires no arguments, it simply wraps whatever msg.value
is sent to it at a 1:1 ratio.
withdraw()
, however, requires one argument: a uint256
that specifies how much WETH to convert back to ETH.
When we call withdraw()
, how does Brownie or web3.py or ethers.js or the browser wallet know how to encode the input correctly? It checks the ABI.
Scroll up and find this code block:
{'constant': False,
'inputs': [{'name': 'wad', 'type': 'uint256'}],
'name': 'withdraw',
'outputs': [],
'payable': False,
'stateMutability': 'nonpayable',
'type': 'function'}
This is the ABI specification for a function called withdraw
. The 'inputs'
key specifies that a single input of type uint256
and the name ‘wad’ is required. The name is not important, but it is used by the Brownie console (among other tools) to give hints about what the function expects.
The ABI also includes information about whether the function is payable and what output it returns.
When we make the call to withdraw()
, the RPC will verify that the input type, order, and length matches the ABI.
Why Bother?
The advantage to understanding how calldata is generated from an ABI is that function inputs and outputs can now be completely deterministic. That is, you can generate calldata off-chain in a very modular way. In the past we’ve used Brownie (using encode_input
) or web3py (using encodeABI
) to generate calldata before building a payload.
The trouble with using Brownie and web3py is that you need to have access to an ABI for both these methods. It’s usually no trouble to fetch one from a block explorer and create a contract helper object, but if you can avoid the process, you should!
eth_abi
There is a very useful, and very minimal, module called eth_abi. This module does one thing very well, it encodes and decodes values against a known ABI type. This does not mean that you need a full ABI to use it, only that you must know the specific type of the value you are encoding or decoding.
Lesson Roadmap
In this lesson we will build a replacement for the refresh_pools_sync()
function in the previous bot project, using the decode_abi()
function from eth_abi
.