With the Aerodrome V2 and V3 pool helpers largely built, we turn our attention to the topic of pool management.
When youโre doing one-off tasks, manually creating a pool helper is just fine. You know the address and other relevant properties like the fee, token address, and factory.
Itโs easy enough to keep the relevant bits in your head, and entering them manually isnโt a problem.
But when youโre building something big, the volume blows up and you just canโt rely on this approach.
I ran into this problem when I began scaling bots to cover multiple DEX. My solution was a pool manager that creates and tracks liquidity pools associated with a particular factory address. I chose the name โmanagerโ despite โfactoryโ being the more common term for this sort of class, because โfactoryโ already has a special meaning onchain.
Iโve already built three pool helpers (Uniswap V2, Uniswap V3, and Curve V1), and now itโs time to build one for Aerodrome.
Recall from Part I: Overview that Aerodrome V2 is forked from Velodrome V2, and Aerodrome V3 is forked from Uniswap V3.
I already have Uniswap V2 and V3 pool managers, so I will subclass them as a quick hack to reuse some methods:
from ..aerodrome.pool import AerodromeV2Pool, AerodromeV3Pool
from ..uniswap.managers import (
UniswapV2PoolManager, UniswapV3PoolManager
)
class AerodromeV2PoolManager(UniswapV2PoolManager):
Pool: TypeAlias = AerodromeV2Pool
[...]
class AerodromeV3PoolManager(UniswapV3PoolManager):
Pool: TypeAlias = AerodromeV3Pool
[...]
Manager-Specific Pool Creation
Iโve been refactoring heavily behind the scenes to improve the friendliness of using these classes. One improvement is making the pool managers aware of which class to use to create new pools.
By default, UniswapV2PoolManager
will create pools of type UniswapV2Pool
, and UniswapV3PoolManager
will create pools of type UniswapV3Pool
.
I want the Aerodrome pool managers to create the Aerodrome pool flavor instead of the โvanillaโ Uniswap pools, so I set a TypeAlias to link a specific class (AerodromeV2Pool
and AerodromeV3Pool
) to a generic name (Pool). In this way, the correct pool type for a specific manager can be built generically using self.Pool(*args)
instead of overriding methods and types.
Pool Address Lookup
Since Aerodrome uses proxy contracts, the pool contract address lookup needs to follow a different path. Previous entries in the Aerodrome series describe the deterministic pool address method used by the factory (OpenZeppelin Clone contracts).
The pool manager is used for many purposes, but a critical one is to allow a transaction simulation to fetch a pool for each hop along a particular sequence of tokens involved in a swap. A router contract is typically token-focused, and abstracts away the complexities of pool contracts. Swaps typically consist of an input of some token, and an output of some token. The pools involved in the swap are typically hidden from the perspective of the transaction, and the router contract manages which pools will be used for the swap.
The router contract typically does this in one of two ways:
Look up the pool at the factory
Calculate the pool address that would hold the tokens
For an example of the second method, refer to the Uniswap Universal Router source code, particularly the computePoolAddress function.
A pool manager performs a similar role to the contracts above, at least from the perspective of pool address lookup. They track locally-built pools, ensuring that only one pool object is built for a given address, and allow other classes to call on them to get a reference to these pools.
But we know that pools sometimes need to be found by tokens instead of a specific address, so the managers must also expose trait-specific lookup methods. In this way, a pool manager should support asking for the pool directly (by address) or indirectly (by tokens, fees, or feature flags).
Aerodrome V2 pools vary by their tokens and the stable flag.
Aerodrome V3 pools vary by their tokens and tick spacing. This differs from the Uniswap V3 method, which sets the salt based on the tokens and the fee.
You can verify the V2 method at the PoolFactory.sol contract:
bytes32 salt = keccak256(abi.encodePacked(token0, token1, stable));
// salt includes stable as well, 3 parameters
pool = Clones.cloneDeterministic(implementation, salt);
You can verify the V3 method at the CLFactory.sol contract:
pool = Clones.cloneDeterministic({
master: poolImplementation,
salt: keccak256(abi.encode(token0, token1, tickSpacing))
});