Pool identity
A pool is(token0, token1, poolType) — nothing else. There are no fee tiers: exactly one canonical pool per combination. Lookup is getPool(tokenA, tokenB, poolType), which answers with the tokens in either order.
Listing a pool
The factory creates pools only for accounts holding its creator role:NotAPoolCreator() — and so does initialize on an unpriced pool. What holds the role are listers: contracts that apply a listing policy and create and price a pool in a single call.
Contracts that hold the role directly — such as the launchpad — call
createPoolWithTerms themselves. The role holders are enumerable on-chain (getRoleMemberCount / getRoleMember for POOL_CREATOR_ROLE()), and role changes emit OpenZeppelin’s RoleGranted / RoleRevoked.
The position manager offers the same flow to LP frontends, forwarding data to the lister unread:
Before offering pool creation in a UI
- Check the lister, not the user:
factory.canCreatePool(publicLister). A user’s own address is not a creator. - Offer CL only through the public lister.
- Check the type has defaults —
defaultsOf(CL)with a non-zero tick spacing — or creation revertsDefaultsNotSet(). - Check
getPool(a, b, CL)first: an existing, priced pool is simply handed back, so show it as already listed. - Collect an opening price and encode it:
abi.encode(PoolType.CL, sqrtPriceX96). - On a chain with no native wrapper, send no
msg.value.
Pool types are an open set
PoolType is a user-defined uint8, not an enum — deliberately, so the ABI never rejects a value newer than your build:
Address derivation
The deployer, not the factory. Pools are CREATE2-deployed by a separate deployer contract — read its address fromfactory.poolDeployer(). Derivation:
deployer.computePoolAddress(token0, token1, poolType).
Reading a pool
Uniform across every pool type:- Uninitialized pools return zeros, never revert — a pool created directly through the factory exists unpriced until a creator initializes it; listers create and price in one call.
tickon a tickless (STABLE) pool is a measurement of where the price sits, nothing more — the pool holds nothing at it. Never mint against a tick read from a STABLE pool.feeInfo()is backward-looking on dynamic pools: it is the fee the last swap charged. For real quotes, use the Quoter. Fees are in hundredths of a bip and can range up to 999,999 — the units, the splits, and how to work out what a trade actually paid are on Fees.feeToken()names the fee coin:Paidmeans each trade pays its fee in the token it sells;Token0/Token1mean every fee is taken in that slot’s token — from the output side when trading the other way. Fee growth and protocol fees then accrue on that side only.tickSpacingis mutable on CL pools (governance), fixed on CP,0on STABLE.liquidity()is in-range liquidity on CL — unrelated to the total across all ticks.
Positions and resolveTicks
Pool-level positions are keyed keccak256(owner, tickLower, tickUpper). But some pool types override the range: CP pins every position to the spacing-aligned full range; STABLE has exactly one range and ignores the arguments. Before computing a key, ask the pool:
Mint/Burn/Collect events always carry the resolved range.
Tick data for routing engines
The tick tree indexes by the tick itself, nottick / tickSpacing — that is what lets governance retune spacing on a live pool without moving a stored bit:
tickBitmap(int16 word) returns the leaf word; tickTreeRoot() the top. The TickLens walks it for you: getPopulatedTicksInWord(pool, word) returns exact-sized (tick, liquidityNet, liquidityGross) arrays, and wordOf(tick) computes the word — no spacing argument. CL and CP only; a STABLE pool’s whole depth sits at one price and is liquidity().
STABLE-only reads: curveReserve0() / curveReserve1() — the curve’s pricing reserves, deliberately not the pool’s balances (uncollected fees sit alongside), plus amplificationX100() and amplificationRamping().