It’s simple to work on mainnet. The exchanges are well understood and documented, the official contract addresses are easily found, and the degenbot classes work “out of the box” with easy access to their ABIs.
But then you venture to other chains and everything becomes less simple.
One important safety feature that I’ve built into the LiquidityPool
and V3LiquidityPool
helpers is the notion of automatic address checking.
The Uniswap factories make use of the CREATE2 opcode to deploy new pools with deterministic contract addresses. This means that the address for a given contract can be pre-calculated before it is created. That’s useful for exchanges that want to use the same contract address on different chains, and useful for users who want a way to verify that the pool they are interacting with is valid.
The Uniswap Universal Router takes advantage of this technique in the computePoolAddress function, which it uses to verify the calling pool address inside its swap callback.
The pool helpers I’ve written use the same technique when they are loaded. If the pool helper is aware of the values used to salt the CREATE2 opcode, the bytecode used to deploy the pool, and the address of the deploying contract, it can calculate the resulting address and compare it to the value you’ve given. If they both match, you have a valid pool and the helper will continue.
I added a fairly simplistic set of factory contract addresses, ticklens contracts, and routers built into degenbot early on as a means of making the typical use case functional. A user could provide the address of a mainnet V2 or V3 liquidity pool — the helper would recognize the exchange by its factory address, then load the appropriate values.
But as Ethereum moves further down the rollup-centric roadmap, we are increasingly needing to chase down addresses, pool initialization bytecode, and CREATE2 salts.
I took some time this week to build a nice quality-of-life feature — standardized exchange data loading.
The Old Way
In the Base Backrunner project, I provided exchange data that would allow the pool helpers to load the correct defaults for seven exchanges (Uniswap V2, Uniswap V3, Sushiswap V2, Sushiswap V3, Pancakeswap V2, Pancakeswap V3, and Swapbased V2). The value injection code for the two Uniswap exchanges looked like this:
# Uniswap V2
UniswapLiquidityPoolManager.add_pool_init_hash(
chain_id=bot_status.chain_id,
factory_address="0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6",
pool_init_hash="0x96e8...845f", # trimmed for clarity
)
univ2_lp_manager = degenbot.UniswapV2LiquidityPoolManager(
factory_address="0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6"
)
# Uniswap V3
UniswapLiquidityPoolManager.add_pool_init_hash(
chain_id=bot_status.chain_id,
factory_address="0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
pool_init_hash="0xe34f...8b54", # trimmed for clarity
)
univ3_lp_manager = degenbot.UniswapV3LiquidityPoolManager(
factory_address="0x33128a8fC17869897dcE68Ed026d694621f6FDfD",
snapshot=snapshot,
)
And dealing with the associated TickLens contract was similar.
The ultimate goal was to provide the values associated with a particular exchange so that new pools could be loaded and verified against their canonical contract addresses.