New Feature Roundup
I continue to extend the feature set of the degenbot code base. Many changes aren’t big enough for a dedicated post, but there is a risk of them being overlooked and untested if I just push them to Github and then move to the next shiny thing.
Here is a list of some interesting new developments that you might enjoy.
The Great Brownie Replacement is a success! I have retooled the classes and confirmed that the current code base operates correctly with or without Brownie. There are some minor web3.py differences specific to version 6, but that’s been resolved internally so this should be largely transparent to users.
The code base will work out-of-the-box with Brownie, so you can continue to use this platform if it is already working. If you want to extract Brownie from your setup, in favor of a purely web3.py approach, take the following steps:
After loading the
degenbotmodule, pass a
web3object with the
degenbot.set_web3()method. This object will be used within the code base everywhere it interacts with the live blockchain. If a
web3object is provided in this way, the Brownie module is not loaded. If your Python environment does not have Brownie installed, you may see “Unresolved import” warnings in your IDE which are harmless since these code blocks are skipped at runtime.
If you are using Brownie’s
accountsmodule to manage keys for sending and signing transactions, you may replace it with a lower-level method like the eth_account module. I’ve used the
sign_transactionmethod in several examples here, so you should be comfortable with this approach. You can discover the private key for a Brownie-generated account on the console:
(.venv) [btd@main bots]$ brownie console Brownie v1.19.3 - Python development framework for Ethereum BotsProject is the active project. >>> acct = a.load('test') Enter password for "test": >>> acct.private_key '0x....'
If you are relying on Brownie’s built-in storage for retrieving unique contract ABIs, you should develop a local repository for these. The degenbot code base includes standard ABIs for ERC-20 tokens, Chainlink price feeds, and all Uniswap/Sushiswap contracts, but more exotic contracts may require the use of a custom
abiparameter to the constructor. If this is true for you, you can maintain a simple py file that holds ABIs as needed in native Python format (a list), or JSON-formatted text that can be processed with
json.loads(). You may use degenbot’s Uniswap ABIs import as an example.
Transaction Simulation Fixes
I caught a few
TransactionError exceptions from the
UniswapTransaction helper used heavily in the recent mempool backrun bot. They were mostly related to unaccounted WETH unwrapping at the router, which I’ve fixed with this commit.
Pre-Calculation Profit Check
I use SciPy’s
minimize_scalar function for calculating optimal amounts for an arbitrage path. It’s very effective, but it does not have an “escape valve” to stop early if the driven function has no profitable optimal value. It simply iterates and returns the solution, which is often a negative value since markets are kinda-sorta efficient and arbitrage opportunities do not last long.
I was working on optimizing the 2-pool arbitrage case, and realized that I could implement a very simple pre-calculation check to discard calculations where no profitable transaction was possible at the current pool states.
A necessary prerequisite for a profitable arbitrage is that the current “price” of both pools must have some delta. First, we need to define price in a standard way. Uniswap liquidity pools hold two tokens, which are referenced as
token1 (sorted by address). Uniswap V2 pools have no concept of price, they simply express an instantaneous swap rate for a given token as a function of pool reserves. Uniswap V3 pools, however, have a built-in concept of price. Though they abstract it as a tick or a square root price for more efficient calculations. Read through the Uniswap V3 contract series for more on this!
Uniswap V3 defines price as the ratio of
token1 received for a given
token0 input. Good enough for me!
Uniswap V3 Price
The pool records the current square root price as an Q64.96 value. This Q-formatted value represents a high-precision floating point number with 64 bits given for the integer portion of, and 96 bits given for the fractional portion. The total bit size for the number is 160 (64+96), which aligns with the
uint160 value named
sqrtPriceX96, which is defined in the Uniswap V3 Pool contract.
To get from
sqrtPriceX96 to a floating point value, apply the following conversion from Uniswap’s V3 Math Primer:
sqrtPrice = sqrtPriceX96 / (2**96)
Square both sides
sqrtPrice**2 = (sqrtPriceX96 / (2**96))**2
Which resolves to
price = sqrtPriceX96**2 / (2**(96*2))
price = sqrtPriceX96**2 / (2**(192))
Uniswap V2 Price
Getting price from a V2 pool is much easier.
price = reserves_token1 / reserves_token0
Unhealthy At Any Trade Size
With this concept of price, let’s evaluate a 2-pool arbitrage path using Uniswap V2 pools holding the WBTC-WETH pair.