# Deterministic V4 discovery: finding every hookless pool without a factory, a list, or a deployer to trust

> Canonical: https://blazephoenix.xyz/learn/deterministic-v4-discovery
> License: CC BY 4.0 (attribution + link) · © 2026 BlazePhoenix
> Updated: 2026-08-18



To discover a Uniswap V2 or V3 pool you derive an address: CREATE2 makes the pool of a token pair a pure function of factory, salt and init code, so you can compute where it MUST live and check (that is the sibling technique — see create2-discovery). V4 deletes the thing that derivation stood on. There are no per-pool contracts and no factories: every pool of every deployer lives as a storage entry inside ONE PoolManager singleton per chain. There is nothing to scan.

That sounds like it makes discovery harder. It makes it cleaner. Because there is exactly one creation path — PoolManager.initialize(PoolKey, sqrtPrice) — every pool that has ever existed, from any deployer, is identified the same way: PoolId = keccak256(abi.encode(PoolKey)). The pool is not something you look up in a directory; it is the hash of its own definition. The deployer is erased from the trust model — you never ask who made a pool, only whether the id you derived reads back a live price.

## The meta-pattern: you cannot enumerate, but you can derive

A PoolKey is five fields — currency0, currency1, fee, tickSpacing, hooks. The singleton keys its state by the keccak of those five, and — this is the load-bearing fact — the PoolKeys themselves are never stored on-chain; they exist only in the Initialize event log. A contract cannot read logs. So there is no on-chain function that returns "all pools for USDC/WETH": the set is genuinely unenumerable from inside the EVM.

But the same asymmetry that forbids enumeration grants derivation. Serious hookless liquidity clusters on a tiny, industry-standard grid of (fee, tickSpacing) tiers. For a pair, we can DERIVE the four canonical PoolIds — hooks fixed to address(0) — and ask the singleton whether each exists. That is not a lookup against a list we maintain; it is arithmetic anyone reproduces, and it works for a pool deployed one block ago by someone we have never heard of.

## The whole discovery, as one equation and one read

The id is one hash over the five-field key; existence is one storage read on the singleton at the slot that key hashes to. Both are pure functions — no registry, no admin, no gas:

> equation: id=keccak256(abi.encode(c0,c1,fee,ts,0)),exists⇔sqrtPid≠0 — PoolId over the hookless (hooks = 0) canonical key; the pool exists iff extsload of its slot0 (base = keccak256(abi.encode(id, 6)), low 160 bits) returns a non-zero sqrtPriceX96. Verified live on Base: for WETH/USDC all four canonical tiers read non-zero, and this slot0 read matches the canonical StateView.getSlot0 to the wei at the same block.

## One call, zero gas, deployer-blind

The Hub's V4 derive-scan sorts the pair, derives the canonical hookless ids, and reads them all through one batched extsload on the singleton, returning the tiers that hold a live price and liquidity. It is a view function — an eth_call — so it costs no gas, and it is deterministic: the same inputs give the same ids at every block. What it replaces is worth naming: not a 99-call factory sweep, and not an admin allowlist of "approved" pools. There is nothing to approve, because a pool cannot lie about its own id.

The discovered pools merge into the router's candidate set alongside V2/V3 venues; the best output wins, and the contract-enforced iron floor makes admission safe without trusting the pool — the worst a bad pool can do is fail to fill or deliver less, which the floor catches. Identity-free discovery, execution-time safety.

## The honest limit: hooked pools are physics, not a gap

The derive-scan finds HOOKLESS pools. Hooked pools carry an arbitrary hook contract, and a hook address is 160 bits with only its low 14 bits constrained (those encode the hook's permission flags, mined into the address via CREATE2). The other ~146 bits are free. You cannot iterate 2^146 addresses, and the only record of which hooks exist for a pair is the Initialize log a contract cannot read. So hooked-pool enumeration on-chain is impossible — for anyone, not just us. It is a property of V4's design, and pretending otherwise would be the kind of unverifiable claim this protocol exists to avoid.

Measured on Base: for WETH/USDC, all four canonical hookless tiers exist. For memecoins the picture is exactly what the design predicts — BRETT/WETH and DEGEN/WETH each expose one hookless canonical pool that the derive-scan finds; a token whose V4 liquidity lives behind a hook (dynamic fees, anti-snipe) exposes none to this call. Those are reachable only through an off-chain feed of the Initialize logs — which stays allowlist-free, because a submitted key still self-proves against the singleton.

## The bits that classify what you cannot enumerate

Enumeration is impossible, but classification is free and objective — three small bit-patterns handle every V4 pool without trusting a soul. The hook's low 14 bits say what it may do (beforeSwap, afterSwap, return-delta) straight from its address. The pool's fee field says whether the fee is dynamic: bit 23, the flag 0x800000, distinguishes a dynamic-fee pool (its live fee set by the hook) from a static one, and static fees never reach that bit. And the current fee of a dynamic pool is not a mystery either — it lives in slot0's lpFee field, read in the same extsload that reads the price. Derive the id, read the slot, and the pool's price, liquidity and live fee are all in hand — no oracle, no registry, no permission.

**Verify it yourself:** reproduce a derivation end to end (Base WETH/USDC, 0.05% tier): PID=$(cast keccak $(cast abi-encode "f(address,address,uint24,int24,address)" 0x4200000000000000000000000000000000000006 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 500 10 0x0000000000000000000000000000000000000000)); cast call 0xa3c0c9b65bad0b08107aa264b0f3db444b867a71 "getSlot0(bytes32)(uint160,int24,uint24,uint24)" $PID --rpc-url https://mainnet.base.org — a non-zero sqrtPriceX96 is the pool, derived not looked up

Related: https://blazephoenix.xyz/learn/create2-discovery · https://blazephoenix.xyz/learn/uniswap-v4-end-to-end · https://blazephoenix.xyz/learn/on-chain-quoting · https://blazephoenix.xyz/learn/phantom-liquidity · https://blazephoenix.xyz/learn/stateless-contracts · https://blazephoenix.xyz/learn/measured-not-nominal
