With the skeleton of the basic LP Helper built, now we start working on adding features.
The key reason to have an LP helper is to define a common interface that bot builders can use to retrieve and store data relevant to liquidity pools.
Let’s add a few attributes to keep track of the pool’s factory address and tick data.
def __init__(self, address: str):
self.address = to_address(address)
try:
self._brownie_contract = Contract(address=address)
except:
try:
self._brownie_contract = Contract.from_explorer(
address=address, silent=True
)
except:
try:
self._brownie_contract = Contract.from_abi(
name="", address=address, abi=V3_LP_ABI
)
except:
raise
try:
self.token0 = self._brownie_contract.token0()
self.token1 = self._brownie_contract.token1()
self.fee = self._brownie_contract.fee()
self.slot0 = self._brownie_contract.slot0()
self.liquidity = self._brownie_contract.liquidity()
self.tick_spacing = self._brownie_contract.tickSpacing()
self.sqrt_price_x96 = self.slot0[0]
self.tick = self.slot0[1]
self.factory = self._brownie_contract.factory() # NEW
self.tick_data = {} # NEW
except:
raise
During instantiation, I generally assign an empty list or dictionary for data structures that will be populated later, and None
for variables that will be set later.
The plan is for tick_data
to be a dictionary that stored liquidity information associated with a particular tick, keyed by that tick.
Tick Gathering
To retrieve that info, we also need to build a little helper to query the TickLens contract. So let’s do that!
tick_lens.py
from abc import ABC, abstractmethod
from brownie import Contract
from brownie.convert import to_address
class TickLens(ABC):
def __init__(
self,
address="0xbfd8137f7d1516D3ea5cA83523914859ec47F573"
):
self.address = to_address(address)
try:
self._brownie_contract = Contract(address=address)
except:
try:
self._brownie_contract = Contract.from_explorer(
address=address, silent=True
)
except:
raise
The TickLens()
constructor assumes that the TickLens contract is deployed to mainnet at the standard address, though it allows you to override that if you’d like.
Now back in the LP helper, we can store a Brownie Contract object that allows us to query the the TickLens.
However let’s think ahead to memory use. Eventually we will have thousands of V3 pools to manage, and generating a new Contract object for each pool is a big waste of space. So in the constructor arguments, let’s include another optional argument that takes a Contract instance called lens
. If it’s found, the LP can store this directly instead of generating a new one.
Simple changes:
# add this line to the imports
from .tick_lens import TickLens
# change the old constructor from this...
def __init__(self, address: str):
# ... to this
def __init__(self, address: str, lens: Contract = None):
And then inside the constructor, add a block of code to build and store the lens contract:
if lens:
self.lens = lens
else:
try:
self.lens = TickLens()
except:
raise
Now when the LP helper is built, it will have a way to ask the TickLens about itself. Now let’s define a function that asks the TickLens for liquidity information about itself:
def get_tick_data_at_word(self, word_position: int):
"""
Gets the initialized tick values at a specific word
(a 32 byte number representing 256 ticks at the tickSpacing
interval), then stores the liquidity values in the `self.tick_data`
dictionary, using the tick index as the key.
"""
try:
tick_data = self.lens._brownie_contract.getPopulatedTicksInWord(
self.address, word_position
)
except:
raise
else:
for (tick, liquidityNet, liquidityGross) in tick_data:
self.tick_data[tick] = liquidityNet, liquidityGross
return tick_data
And one more function that uses the TickBitmap library from univ3py to calculate the word_position
parameter:
def get_tick_bitmap_position(self, tick) -> Tuple[int, int]:
"""
Retrieves the wordPosition and bitPosition for the input tick
This function corrects internally for tick spacing!
e.g. tick=600 is the 11th initialized tick for an LP with
tickSpacing of 60, starting at 0.
Calling `get_tick_bitmap_position(600)` returns (0,10), where:
0 = wordPosition (zero-indexed)
10 = bitPosition (zero-indexed)
"""
return TickBitmap.position(tick // self.tick_spacing)
Now I have all I need to retrieve and store liquidity data at different ticks, all from the LP helper. Add a few more lines to the constructor to automatically fetch the current “word” from TickLens at startup:
try:
self.token0 = self._brownie_contract.token0()
self.token1 = self._brownie_contract.token1()
self.fee = self._brownie_contract.fee()
self.slot0 = self._brownie_contract.slot0()
self.liquidity = self._brownie_contract.liquidity()
self.tick_spacing = self._brownie_contract.tickSpacing()
self.sqrt_price_x96 = self.slot0[0]
self.tick = self.slot0[1]
self.factory = self._brownie_contract.factory()
self.tick_data = {}
self.tick_word, _ = self.get_tick_bitmap_position(self.tick) # NEW
self.get_tick_data_at_word(self.tick_word) # NEW
except:
raise
Now after I build an LP helper, I can inspect the state of tick_data
and see the liquidity deltas stored at each tick crossing:
(.venv) [devil@dev bots]$ brownie console --network mainnet-local
Brownie v1.19.2 - Python development framework for Ethereum
BotsProject is the active project.
Brownie environment is ready.
>>> import degenbot as bot
>>> lp = bot.uniswap.v3.V3LiquidityPool(
'0xCBCdF9626bC03E24f779434178A73a0B4bad62eD'
)
>>> lp.tick_data
{
245940: (76701235421656, 76701235421656),
246120: (1943231882441, 1943231882441),
246360: (1315914775060493, 1315914775060493),
246420: (69923058683730, 69923058683730),
246540: (837767553892, 837767553892),
[...]
260760: (-29919515125784841, 29919515125784841),
260820: (-1590575951225889, 1590575951225889),
260940: (-28055610380093, 28055610380093),
261000: (-207031583514061, 207031583514061),
261060: (-98282790975871, 98282790975871)
}
Let’s also make a change with the future in mind. The previous version had a method called update
, but I’d like to make a distinction between an update executed by the LP helper itself and an update of the LP helper via externally-provided data. So I’m renaming update
to auto_update
, in anticipation of adding another method later called external_update
that will accept external values instead of querying the blockchain.
Exact Input Swap Prediction
Now let’s tackle a trickier topic — calculating swap amounts. The functions from the UniswapV3 supporting libraries (SwapMath in particular) allow us to calculate how swaps will execute on-chain. So let’s borrow and steal from the Pool contract itself, taking from the various libraries as needed to do the swap calculations. To simplify as much as possible, I will concentrate only on a single type of swap (exact input in a single pool).
I will use a familiar function name from the V2 Liquidity Helper, calculate_tokens_out_from_tokens_in
:
You’ll recall from my lessons on Swap Prediction (Parts I, II, and III) that calculating swaps on UniswapV3 involved a lot of bookkeeping. Rather than re-implement it with my own methods, I have simply reproduced the calculation behavior of the whole pool contract and supporting libraries in the univ3py module.
This allows us to calculate how swaps will be executed by the on-chain contracts without having to ask the node / RPC.
It makes the code pretty confusing to follow, though. I won’t reproduce it all here, instead just showing a placeholder for the re-implemented swap
functionality of the Pool Contract.
The function calculate_tokens_out_from_tokens_in
will act as a wrapper that accepts degenbot helper objects and arguments, translates them to the appropriate inputs to swap
, and then returns the appropriate data.