Part I of this series covered the basic structure of the Aerodrome exchange, including the major contracts.
I have been doing a lot of heavy refactoring in the degenbot repository this week, which has been very helpful as a re-introduction to parts of the code base that I haven’t touched in a while.
The ultimate goal of the Aerodrome study is to build and utilize a liquidity pool helper that we can interact with and replicate onchain behavior of the Solidly-forked pools. I have made initial code commits to the solidly directory that will form the base of the pool class.
I will refer to the Solidly-forked contracts as Aerodrome V2. The concentrated liquidity contracts will be referred to as Aerodrome V3 when we come to them.
Deterministic Pool Addresses
The Uniswap pools in degenbot implement a nice feature that can verify the pool address against the deterministic address generated by the factory at deploy time. This means that the local pool helper can verify that the address it was given against the factory address that generated it. If there is a mismatch, the pool will refuse to build.
As an aside, the reason I’ve programmed this address verification feature is to guard against fake pools that the bot might otherwise generate when watching live transactions through the mempool. I have exposed a verify_address
boolean in the constructor, so you still have the option to raw-dog pools as you desire, but I recommend leaving this on and working with me to write and document verified exchange deployments so everyone benefits.
Prior to the exchange deployment data feature, it was difficult to standardize around this with new helpers. But now it should be simple to load the exchange in, replicate the address generation method from the contract, and forever be assured that the pool helper we’re loading has a valid address per the canonical factory contract.
By inspecting the PoolFactory contract, we discover the createPool function which is responsible for making new V2 pools:
function createPool(address tokenA, address tokenB, bool stable) public returns (address pool) {
if (tokenA == tokenB) revert SameAddress();
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
if (token0 == address(0)) revert ZeroAddress();
if (_getPool[token0][token1][stable] != address(0)) revert PoolAlreadyExists();
bytes32 salt = keccak256(abi.encodePacked(token0, token1, stable)); // salt includes stable as well, 3 parameters
pool = Clones.cloneDeterministic(implementation, salt);
IPool(pool).initialize(token0, token1, stable);
_getPool[token0][token1][stable] = pool;
_getPool[token1][token0][stable] = pool; // populate mapping in the reverse direction
allPools.push(pool);
_isPool[pool] = true;
emit PoolCreated(token0, token1, stable, pool, allPools.length);
}
I’ve highlighted the relevant line above. Clones references the OpenZeppelin Clones contract, which deploys EIP-1167 minimal proxy contracts called “clones”. A minimal proxy contract simply delegates call calls to the “implementation” contract at some fixed address.
All Aerodrome V2 pools deployed to date follow this pattern, salting the address algorithm with a hashed close-packed encoding of the sorted token addresses and the boolean “stable” flag. They all delegate to the implementation contract at address 0xA4e46b4f701c62e14DF11B48dCe76A7d793CD6d7.
So the very first thing I need is a method of replicating the deterministic address of these clones. The best resource I found for understanding the EIP-1167 clone structure is the Rareskills explainer. I have written a function called eip_1167_clone_address
that implements this same technique with a variable implementation argument to vary the address as needed:
def eip_1167_clone_address(
deployer: ChecksumAddress | str | bytes,
implementation_contract: ChecksumAddress | str | bytes,
salt: bytes,
) -> ChecksumAddress:
"""
Calculate the contract address for an EIP-1167 minimal proxy contract deployed by `deployer`,
using `salt`, delegating calls to the contract at `implementation` address.
References:
- https://github.com/ethereum/ercs/blob/master/ERCS/erc-1167.md
- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Clones.sol
- https://www.rareskills.io/post/eip-1167-minimal-proxy-standard-with-initialization-clone-pattern
"""
MINIMAL_PROXY_CODE = (
HexBytes("0x3d602d80600a3d3981f3")
+ HexBytes("0x363d3d373d3d3d363d73")
+ HexBytes(implementation_contract)
+ HexBytes("0x5af43d82803e903d91602b57fd5bf3")
)
return create2_address(
deployer=deployer,
salt=salt,
init_code_hash=keccak(MINIMAL_PROXY_CODE),
)
I will gloss over the details of the create2_address
implementation, but you can review the OpenZeppelin CREATE2 opcode documentation for more, or inspect the degenbot source.