One of the most irritating things about running a bot is the time-consuming need to look up addresses for liquidity pools and tokens. Much of this is done by hand, which is terribly inefficient and error-prone, since a typo or a misconfiguration can lead to a dysfunctional bot and weird errors that are hard to diagnose.
A long-standing item on my to-do list has been automating the process of retrieving pool and token addresses from the Factory contract. Until now I’ve been doing this by hand, but learning about multicall has solved that problem.
So first I’ll talk about multicall, then use it to build a script that retrieves addresses for every known liquidity pool on Avalanche (TraderJoe, SushiSwap, and Pangolin), plus the token addresses inside those pools. Once we have this information, it suddenly becomes much easier to design an algorithm that answers the question “what arbitrage pathways are available on this blockchain?”
Multicall
I’m not sure who implemented multicall first, but the MakerDAO multicall GitHub repo has commits dating back to 2018. Many examples are forked from this codebase, so I’ll assume they were either the original authors or early pioneers.
Multicall allows multiple read-only requests (calls) to be made to a single smart contract using a single request at the node. This has three advantages:
Calls are grouped together, so you only use a single eth_call at the node per batched request. This saves you from burning through API requests if you're using a 3rd party RPC and have a rate limit.
Since all requests are sent and returned at once, latency becomes less critical. Instead of waiting
avg_latency * 2 * num_requests
, you wait onlyavg_latency * 2
(assuming the multicall returns in similar time vs. a single request)All requests from a multicall are returned from the same block. This eliminates errors caused by data changing over multiple blocks as your individual calls return.
Multicall in Brownie
Brownie provides native multicall functionality which you can use if you set a special key in your network config called multicall2
.
It’s easy enough, open .brownie/network-config.yaml
and add this line in your Avalanche network block:
multicall2: '0xcA11bde05977b3631167028862bE2a173976CA11'
Mine looks like this, for reference:
- name: Avalanche
networks:
- chainid: 43114
explorer: https://api.snowtrace.io/api
host: https://speedy-nodes-nyc.moralis.io/[redacted]/avalanche/mainnet
multicall2: '0xcA11bde05977b3631167028862bE2a173976CA11'
id: moralis-avax-main
name: Mainnet (Moralis)
The easiest way to execute a multicall request in Brownie is to instantiate a multicall object using with
and then build all of your calls inside a list comprehension. The calls will be batched together through multicall, and the results stored inside that list.
A Brief Introduction to List Comprehensions
If you’ve never worked with a Python list comprehension, you’re in for a real treat. It allows you to quickly build a complex list with a single line of code, as long as you have an iterable object to feed data into it.
As a very simple example, please refer to the list comprehension I use inside the flash_borrow_to_lp_swap
arbitrage helper for degenbot (GitHub link):
self.token_path = [token.address for token in self.tokens]
The general format for a list comprehension is [expression for item in iterable]
expression
is any valid expression allowed by Python, such asx+1
,x == 2
(evaluates toTrue
orFalse
),some_function()
, or even an inline lambda.item
is simply a placeholder reference to some element from the iterable that you pass into your expressioniterable
is any iterable object that you can loop through
At a high level, you can think of a list comprehension as flowing from the right side to the left. You begin with an iterable object, pick an item out of it, then feed it into the expression. Whatever that expression does with it is then saved to the list, and the process repeats.
Let’s take a simple math example:
>>> numbers = [0,1,2,3,4,5,6,7,8,9,10]
>>> [f"Is {number} even? {number % 2 == 0}" for number in numbers]
['Is 0 even? True',
'Is 1 even? False',
'Is 2 even? True',
'Is 3 even? False',
'Is 4 even? True',
'Is 5 even? False',
'Is 6 even? True',
'Is 7 even? False',
'Is 8 even? True',
'Is 9 even? False',
'Is 10 even? True']
I define a list called numbers
, which holds integers from 0 to 10. The comprehension iterates through the list, pulling out the next integer and storing it as a variable called number
. Then it feeds that integer into the expression f"Is {number} even? {number % 2 == 0}"
, which is an f-string with an inline boolean expression that returns True
if the number is even, and False
if the number is odd. Finally the list comprehension aggregates all of the results inside a new list.
You can also add filters to exclude stuff from the iterable. Let’s say we hate the number 3 for some reason, and want to ignore it:
>>> numbers = [0,1,2,3,4,5,6,7,8,9,10]
>>> [f"Is {number} even? {number % 2 == 0}" for number in numbers if number != 3]
['Is 0 even? True',
'Is 1 even? False',
'Is 2 even? True',
'Is 4 even? True',
'Is 5 even? False',
'Is 6 even? True',
'Is 7 even? False',
'Is 8 even? True',
'Is 9 even? False',
'Is 10 even? True']
For more on list comprehensions, I recommend checking out Real Python or other free resources.
Getting Pool Data with Multicall
Now let’s get to the code!