Degen Code

Degen Code

Share this post

Degen Code
Degen Code
Uniswap V3 — LP Helper (Part II)
Copy link
Facebook
Email
Notes
More

Uniswap V3 — LP Helper (Part II)

Tick and Liquidity Fetching, Exact Input Swap Predictions

Nov 24, 2022
∙ Paid
8

Share this post

Degen Code
Degen Code
Uniswap V3 — LP Helper (Part II)
Copy link
Facebook
Email
Notes
More
4
Share

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.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 BowTiedDevil
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share

Copy link
Facebook
Email
Notes
More