Parts I and II of the Aerodrome series outlined the overall architecture of the DEX and an initial sketch of the V2 pool helper.
Today we’ll tackle the Aerodrome V3 pool helper, which is based on the Velodrome Slipstream Concentrated Liquidity contracts. Slipstream was originally forked from Uniswap V3, which I’ve covered in great detail here already.
Since the Uniswap V3 pool helper is already built, the effort here will be mostly focused on identifying the differences between Aerodrome V3 and the original, reusing the shared code, and allowing for differences wherever possible to avoid rework.
Deterministic Pool Address
The pool contracts for Aerodrome V3 are minimal proxy contracts aka “clones”, and built from the same OpenZeppelin Clones contract discussed in the previous post.
So we get to reuse that same CREATE2 address verification, with only a slight difference in the salt structure. Where V2’s salt was built from the token addresses and the stable boolean flag, V3’s salt is built from the token addresses and the tick spacing.
def generate_aerodrome_v3_pool_address(
deployer_address: str | bytes,
token_addresses: Sequence[str | bytes],
implementation_address: str | bytes,
tick_spacing: int,
) -> ChecksumAddress:
"""
Get the deterministic V3 pool address generated by CREATE2.
Uses the token address to generate the salt. The token addresses
can be passed in any order.
Adapted from https://github.com/aerodrome-finance/slipstream/blob/main/contracts/core/CLFactory.sol
and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol
"""
sorted_token_addresses = sorted(
[
HexBytes(address) for address in token_addresses
]
)
salt = keccak(
eth_abi.abi.encode(
("address", "address", "int24"),
[*sorted_token_addresses, tick_spacing],
)
)
return eip_1167_clone_address(
deployer=deployer_address,
implementation_contract=implementation_address,
salt=salt,
)
slot0
The slot0
data struct is named because it is held at the first storage position (slot 0), and it holds several packed state values that are frequently updated during swaps. We care about the first two values, sqrtPriceX96
and tick
, but there are others contained within the struct and which get returned when we call the public getter method slot0()
.
The first difference we find with Aerodrome V3 is that the slot0 struct is built differently. It is defined by CLPool.sol as:
struct Slot0 {
uint160 sqrtPriceX96;
int24 tick;
uint16 observationIndex;
uint16 observationCardinality;
uint16 observationCardinalityNext;
bool unlocked;
}
Whereas UniswapV3Pool.sol defines it:
struct Slot0 {
uint160 sqrtPriceX96;
int24 tick;
uint16 observationIndex;
uint16 observationCardinality;
uint16 observationCardinalityNext;
uint8 feeProtocol;
bool unlocked;
}
Another well-known Uniswap fork, Pancakeswap, makes yet another similar-but-different modification to the struct by changing the feeProtocol from uint8
to uint32
.
Aerodrome does not include the feeProtocol
value at all. In any case, when we work with these forked pools, we have to be aware of and handle that difference.
We can handle it in two ways:
Fetch it with a validated Contract object
Make a direct call over JSON-RPC