- name
- pool-finder
- description
- Use when discovering Uniswap pools, querying pool state, analyzing TVL/volume, finding optimal swap routes, or inspecting pool parameters. Covers V3 Factory queries, V4 PoolManager state, subgraph queries, and cast/forge inspection commands.
# Uniswap Pool Discovery & Analysis
## Key Addresses (Ethereum Mainnet)
| Contract | Address |
|----------|---------|
| V3 Factory | `0x1F98431c8aD98523631AE4a59f267346ea31F984` |
| V3 QuoterV2 | `0x61fFE014bA17989E743c5F6cB21bF9697530B21e` |
| V3 SwapRouter02 | `0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45` |
| V3 NonfungiblePositionManager | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` |
| V4 PoolManager | `0x000000000004444c5dc75cb358380d2e3de08a90` |
## V3 Pool Discovery
### Factory Query
```solidity
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
IUniswapV3Factory factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
// Fee tiers and their tick spacings:
// 100 (0.01%) — tickSpacing 1 — stablecoin pairs
// 500 (0.05%) — tickSpacing 10 — correlated pairs, high volume
// 3000 (0.30%) — tickSpacing 60 — standard pairs
// 10000 (1.00%) — tickSpacing 200 — exotic / low-volume pairs
address pool = factory.getPool(tokenA, tokenB, fee);
```
### Using cast
```bash
# Find ETH/USDC 0.3% pool on Ethereum mainnet
cast call 0x1F98431c8aD98523631AE4a59f267346ea31F984 \
"getPool(address,address,uint24)(address)" \
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
3000
# Read pool slot0 — returns (sqrtPriceX96, tick, observationIndex, observationCardinality,
# observationCardinalityNext, feeProtocol, unlocked)
cast call <pool_address> "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)"
# Active liquidity in the current tick range
cast call <pool_address> "liquidity()(uint128)"
# Pool configuration
cast call <pool_address> "fee()(uint24)"
cast call <pool_address> "tickSpacing()(int24)"
cast call <pool_address> "token0()(address)"
cast call <pool_address> "token1()(address)"
# Oracle observation buffer size
cast call <pool_address> "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" | \
awk '{print "observationCardinality:", $4}'
```
### Scanning All Fee Tiers
```bash
FACTORY=0x1F98431c8aD98523631AE4a59f267346ea31F984
TOKEN_A=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 # WETH
TOKEN_B=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # USDC
for FEE in 100 500 3000 10000; do
POOL=$(cast call $FACTORY "getPool(address,address,uint24)(address)" $TOKEN_A $TOKEN_B $FEE)
if [ "$POOL" != "0x0000000000000000000000000000000000000000" ]; then
LIQ=$(cast call $POOL "liquidity()(uint128)")
echo "Fee: $FEE Pool: $POOL Liquidity: $LIQ"
fi
done
```
## V3 High-Volume Pools (Ethereum Mainnet)
```
ETH/USDC 0.05%: 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
ETH/USDC 0.30%: 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8
ETH/USDT 0.05%: 0x11b815efB8f581194ae79006d24E0d814B7697F6
ETH/USDT 0.30%: 0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36
WBTC/ETH 0.30%: 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD
WBTC/ETH 0.05%: 0x4585FE77225b41b697C938B018E2Ac67Ac5a20c0
USDC/USDT 0.01%: 0x3416cF6C708Da44DB2624D63ea0AAef7113527C6
DAI/USDC 0.01%: 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168
DAI/USDC 0.05%: 0x6c6Bc977E13Df9b0de53b251522280BB72383700
```
## V4 Pool Discovery
V4 pools do not have individual addresses. All pool state lives inside the singleton PoolManager. A pool is identified by its `PoolKey`, hashed into a `PoolId`.
### PoolKey Construction
```solidity
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
using PoolIdLibrary for PoolKey;
// currency0 MUST be numerically less than currency1
PoolKey memory key = PoolKey({
currency0: Currency.wrap(token0),
currency1: Currency.wrap(token1),
fee: 3000, // or LPFeeLibrary.DYNAMIC_FEE_FLAG for hook-managed fees
tickSpacing: 60,
hooks: IHooks(hookAddress) // address(0) for vanilla pool
});
PoolId id = key.toId(); // keccak256(abi.encode(key))
```
### Currency Ordering
V4 enforces `currency0 < currency1`. Native ETH is represented as `Currency.wrap(address(0))`, which sorts below any ERC-20 address. For two ERC-20 tokens, the one with the numerically smaller address is `currency0`.
```solidity
function orderCurrencies(address a, address b) pure returns (Currency c0, Currency c1) {
(c0, c1) = a < b
? (Currency.wrap(a), Currency.wrap(b))
: (Currency.wrap(b), Currency.wrap(a));
}
```
### Reading V4 Pool State with cast
```bash
POOL_MANAGER=0x000000000004444c5dc75cb358380d2e3de08a90
# Get slot0: (sqrtPriceX96, tick, protocolFee, lpFee)
cast call $POOL_MANAGER \
"getSlot0(bytes32)(uint160,int24,uint24,uint24)" \
<pool_id>
# Get active liquidity
cast call $POOL_MANAGER \
"getLiquidity(bytes32)(uint128)" \
<pool_id>
# Get liquidity at a specific tick
cast call $POOL_MANAGER \
"getTickLiquidity(bytes32,int24)(uint128,int128)" \
<pool_id> <tick>
# Get tick bitmap word
cast call $POOL_MANAGER \
"getTickBitmap(bytes32,int16)(uint256)" \
<pool_id> <word_position>
```
### Computing a PoolId Offchain
```bash
# Encode the PoolKey struct and hash it
cast keccak $(cast abi-encode \
"(address,address,uint24,int24,address)" \
<currency0> <currency1> <fee> <tickSpacing> <hooks>)
```
## Subgraph Queries
### V3 Subgraph Endpoints
| Network | Subgraph ID |
|---------|-------------|
| Ethereum | `5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV` |
| Base | `43Hwfi3dJSoGpyas9VwNoDAv55yjgGrPpNSmbQZArzMG` |
| Arbitrum | `FbCGRftH4a3yZugY7TnbYgPJVEv2LvMT6oF1fxPe9aJM` |
| Optimism | `Cghf4LfVqPiFw6fp6Y5X5Ubc8UpmUhSfJL82zwiBFLaj` |
| Polygon | `3hCPRGf4z88VC5rsBKU5AA9FBBq5nF3jbKJG7VZCbhjm` |
Endpoint format: `https://gateway.thegraph.com/api/[api-key]/subgraphs/id/<subgraph-id>`
### Top Pools by TVL
```graphql
{
pools(first: 10, orderBy: totalValueLockedUSD, orderDirection: desc) {
id
token0 { symbol decimals id }
token1 { symbol decimals id }
feeTier
liquidity
sqrtPrice
tick
totalValueLockedUSD
volumeUSD
txCount
}
}
```
### Find Pools for a Specific Token
```graphql
{
pools(
where: {
or: [
{ token0: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }
{ token1: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }
]
}
orderBy: totalValueLockedUSD
orderDirection: desc
first: 5
) {
id
token0 { symbol }
token1 { symbol }
feeTier
totalValueLockedUSD
volumeUSD
}
}
```
### Tick-Level Liquidity Distribution
```graphql
{
ticks(
where: { pool: "<pool_id>" }
first: 1000
orderBy: tickIdx
) {
tickIdx
liquidityNet
liquidityGross
}
}
```
### Active Positions
```graphql
{
positions(
where: { pool: "<pool_id>", liquidity_gt: "0" }
first: 100
orderBy: liquidity
orderDirection: desc
) {
id
owner
tickLower { tickIdx }
tickUpper { tickIdx }
liquidity
depositedToken0
depositedToken1
collectedFeesToken0
collectedFeesToken1
}
}
```
### Historical Volume (Daily Snapshots)
```graphql
{
poolDayDatas(
where: { pool: "<pool_id>" }
orderBy: date
orderDirection: desc
first: 30
) {
date
volumeUSD
tvlUSD
feesUSD
liquidity
sqrtPrice
tick
open
high
low
close
}
}
```
## Optimal Route Finding
### Comparing Fee Tiers
Always query all fee tiers for a pair. The deepest liquidity at the current tick determines the best execution:
```solidity
import {IQuoterV2} from "@uniswap/v3-periphery/contracts/interfaces/IQuoterV2.sol";
IQuoterV2 quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e);
uint24[4] memory fees = [uint24(100), 500, 3000, 10000];
uint256 bestOut;
uint24 bestFee;
for (uint256 i = 0; i < fees.length; i++) {
try quoter.quoteExactInputSingle(
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: WETH,
tokenOut: USDC,
amountIn: 1 ether,
fee: fees[i],
sqrtPriceLimitX96: 0
})
) returns (uint256 amountOut, uint160, uint32, uint256) {
if (amountOut > bestOut) {
bestOut = amountOut;
bestFee = fees[i];
}
} catch {}
}
```
### Multi-Hop Routing
Direct swaps are not always optimal. Common intermediate hops:
```
ETH → WBTC may route as ETH → USDC → WBTC
LINK → UNI may route as LINK → ETH → UNI
Low-cap → Low-cap almost always routes through ETH or USDC
```
```solidity
// Multi-hop quote: tokenA -> WETH -> tokenB
bytes memory path = abi.encodePacked(
tokenA, uint24(3000), WETH, uint24(500), tokenB
);
(uint256 amountOut, uint160[] memory sqrtPriceX96AfterList, uint32[] memory initializedTicksCrossedList, uint256 gasEstimate) =
quoter.quoteExactInput(path, amountIn);
```
### Price Impact Estimation
```solidity
// sqrtPriceX96After from the quote tells you where price lands
// Compare to current sqrtPriceX96 to estimate impact
(uint160 sqrtPriceCurrent,,,,,,) = pool.slot0();
uint256 priceBefore = uint256(sqrtPriceCurrent) * uint256(sqrtPriceCurrent) / (1 << 192);
uint256 priceAfter = uint256(sqrtPriceAfter) * uint256(sqrtPriceAfter) / (1 << 192);
uint256 impactBps = (priceBefore - priceAfter) * 10_000 / priceBefore;
```
## Pool Health Indicators
| Metric | What It Means | Warning Sign |
|--------|--------------|--------------|
| TVL trend | Total value committed by LPs | Declining over 7+ days = liquidity flight |
| Volume / TVL ratio | Fee generation efficiency | < 0.01 daily = stagnant pool |
| Active liquidity concentration | How tightly LPs bracket current price | Wide spread = high slippage for traders |
| Active position count | Number of LPs with in-range liquidity | < 5 positions = fragile liquidity |
| Fee revenue vs IL | LP profitability | Negative = LPs losing money, expect exits |
| Oracle cardinality | V3 observation buffer size (slot0 field 4) | Default 1 = no TWAP history |
| Tick crossing frequency | How often price moves through tick boundaries | Very high = volatile, may deter LPs |
### Reading Oracle Cardinality
```bash
# observationCardinalityNext is the 5th return value of slot0
cast call <pool_address> "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)"
# Increase cardinality for better TWAP support (anyone can call, pays gas)
cast send <pool_address> "increaseObservationCardinalityNext(uint16)" 100
```
## Pool Creation
### V3 Pool Creation
```solidity
IUniswapV3Factory factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
address pool = factory.createPool(tokenA, tokenB, fee);
// Initialize with starting price (sqrtPriceX96 format)
// For 1 token0 = 2000 token1 (e.g., 1 ETH = 2000 USDC with 18/6 decimals):
// sqrtPriceX96 = sqrt(2000 * 1e6 / 1e18) * 2^96
uint160 sqrtPriceX96 = 3543191142285914205922034323215; // example
IUniswapV3Pool(pool).initialize(sqrtPriceX96);
```
### V4 Pool Initialization
```solidity
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
IPoolManager poolManager = IPoolManager(0x000000000004444c5dc75cb358380d2e3de08a90);
PoolKey memory key = PoolKey({
currency0: Currency.wrap(token0),
currency1: Currency.wrap(token1),
fee: 3000,
tickSpacing: 60,
hooks: IHooks(address(0))
});
poolManager.initialize(key, sqrtPriceX96);
```
### sqrtPriceX96 Calculator
```solidity
/// @notice Computes sqrtPriceX96 from a human-readable price ratio
/// @param price The price of token0 in terms of token1 (e.g., 2000 for 1 ETH = 2000 USDC)
/// @param decimals0 Decimals of token0
/// @param decimals1 Decimals of token1
function computeSqrtPriceX96(
uint256 price,
uint8 decimals0,
uint8 decimals1
) pure returns (uint160) {
// adjustedPrice = price * 10^decimals0 / 10^decimals1
// sqrtPriceX96 = sqrt(adjustedPrice) * 2^96
uint256 adjustedPrice = price * (10 ** decimals1) / (10 ** decimals0);
uint256 sqrtPrice = Math.sqrt(adjustedPrice);
return uint160(sqrtPrice << 96);
}
```
## Forge Scripts for Pool Analysis
### Query All Fee Tiers for a Token Pair
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script, console2} from "forge-std/Script.sol";
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
contract PoolScanner is Script {
IUniswapV3Factory constant FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
function run(address tokenA, address tokenB) external view {
uint24[4] memory fees = [uint24(100), 500, 3000, 10000];
string[4] memory labels = ["0.01%", "0.05%", "0.30%", "1.00%"];
for (uint256 i = 0; i < fees.length; i++) {
address pool = FACTORY.getPool(tokenA, tokenB, fees[i]);
if (pool != address(0)) {
(uint160 sqrtPriceX96, int24 tick,,,,,) = IUniswapV3Pool(pool).slot0();
uint128 liquidity = IUniswapV3Pool(pool).liquidity();
View on GitHub