Uniswap v4#
Uniswap v4 introduces a fundamentally different architecture from v2 and v3.
All pools live inside a single PoolManager singleton; positions are ERC-721
tokens managed by the PositionManager; and every pool is uniquely identified
by a PoolKey struct instead of a standalone contract address.
The PoolKey#
A PoolKey is a five-field tuple that uniquely identifies a pool:
from uniswap.types import PoolKey
from uniswap.constants import ETH_ADDRESS, ZERO_HOOK
eth_usdc_pool = PoolKey(
currency0=ETH_ADDRESS, # "0x000...000" — ETH is the zero address in v4
currency1="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC
fee=500, # 0.05% fee tier
tick_spacing=10, # tick spacing that matches the fee tier
hooks=ZERO_HOOK, # "0x000...000" means no hook contract
)
Note
In Uniswap v4, native ETH is represented by the zero address
(0x000...000), not WETH. The two currencies in a pool are always ordered
so that currency0 < currency1 (lexicographic address ordering).
Common fee tiers and their tick spacings:
|
Rate |
Tick spacing |
|---|---|---|
|
0.01% |
1 |
|
0.05% |
10 |
|
0.30% |
60 |
|
1.00% |
200 |
Connecting to Uniswap v4#
Import Uniswap4 and provide a wallet address, private key, and
a mainnet Web3 provider. Omit private_key for read-only use.
from uniswap import Uniswap4
address = "0xYOUR_ADDRESS"
private_key = "0xYOUR_PRIVATE_KEY" # or None for read-only
uni = Uniswap4(
address=address,
private_key=private_key,
provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID",
)
# Or pass a Web3 instance directly:
from web3 import Web3
w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))
uni = Uniswap4(address=address, private_key=private_key, web3=w3)
The PROVIDER environment variable is also accepted:
export PROVIDER="https://mainnet.infura.io/v3/YOUR_PROJECT_ID"
Getting the spot price#
get_token_token_spot_price() returns the current price
of token0 denominated in token1, derived from the pool’s sqrtPriceX96
slot via the v4 StateView contract.
from uniswap.constants import ETH_ADDRESS, ZERO_HOOK
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
# How many USDC per 1 ETH?
price = uni.get_token_token_spot_price(
token0=ETH_ADDRESS,
token1=USDC,
fee=500,
tick_spacing=10,
hooks=ZERO_HOOK,
)
print(f"ETH/USDC spot price: {price:.2f}") # e.g. 3412.54
# Inverse: ETH price in USDC terms
price_inv = uni.get_token_token_spot_price(USDC, ETH_ADDRESS, fee=500, tick_spacing=10)
Quoting a trade#
The quote functions call the on-chain v4 Quoter contract and account for liquidity depth and fees. Use them instead of the spot price whenever you need an accurate estimate for a specific trade size.
Exact input — how much output do I get?#
get_price_input() returns the amount of token1
received for a fixed amount of token0.
from uniswap.constants import ETH_ADDRESS
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
ONE_ETH = 10**18
# How much USDC do I get for 1 ETH?
usdc_out = uni.get_price_input(
token0=ETH_ADDRESS,
token1=USDC,
qty=ONE_ETH,
fee=500,
tick_spacing=10,
)
print(f"1 ETH → {usdc_out / 10**6:.2f} USDC")
For a 2-hop route (e.g. ETH → USDC → USDT), pass a route list of
PoolKey objects instead of fee/tick_spacing:
from uniswap.types import PoolKey
from uniswap.constants import ETH_ADDRESS, ZERO_HOOK
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
eth_usdc = PoolKey(currency0=ETH_ADDRESS, currency1=USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK)
usdc_usdt = PoolKey(currency0=USDC, currency1=USDT, fee=100, tick_spacing=1, hooks=ZERO_HOOK)
usdt_out = uni.get_price_input(
token0=ETH_ADDRESS,
token1=USDT,
qty=ONE_ETH,
route=[eth_usdc, usdc_usdt],
)
Exact output — how much input do I need?#
get_price_output() returns the amount of token0
required to receive a fixed amount of token1.
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
# How much ETH do I need to buy exactly 1000 USDC?
eth_needed = uni.get_price_output(
token0=ETH_ADDRESS,
token1=USDC,
qty=1000 * 10**6, # 1000 USDC in smallest unit
fee=500,
tick_spacing=10,
)
print(f"ETH needed for 1000 USDC: {eth_needed / 10**18:.4f}")
Estimating price impact#
estimate_price_impact() compares the quoted price to
the pool spot price to estimate slippage.
ONE_ETH = 10**18
impact = uni.estimate_price_impact(
token0=ETH_ADDRESS,
token1=USDC,
qty=ONE_ETH,
fee=500,
tick_spacing=10,
)
print(f"Price impact: {impact:.4%}") # e.g. "Price impact: 0.0152%"
if impact > 0.01: # warn above 1%
print("High price impact — consider splitting the trade.")
Making a swap#
Warning
Always verify the expected output (via get_price_input()
or estimate_price_impact()) before executing a swap.
Low-liquidity pools can cause large, unexpected losses.
make_swap_input() executes a swap for a fixed input
amount. Pass the quoted output as qtycap; the client applies its configured
max_slippage tolerance when it builds the transaction.
from uniswap.types import PoolKey
from uniswap.constants import ETH_ADDRESS, ZERO_HOOK
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
ONE_ETH = 10**18
SELL_AMOUNT = ONE_ETH // 10 # 0.1 ETH
pool_key = PoolKey(
currency0=ETH_ADDRESS,
currency1=USDC,
fee=500,
tick_spacing=10,
hooks=ZERO_HOOK,
)
# Uniswap4 applies its configured max_slippage (1% by default) to this quote
expected_usdc = uni.get_price_input(ETH_ADDRESS, USDC, SELL_AMOUNT, fee=500, tick_spacing=10)
tx_hash = uni.make_swap_input(
input_token=ETH_ADDRESS,
output_token=USDC,
qty=SELL_AMOUNT,
qtycap=expected_usdc,
swap_pool_key=pool_key,
)
receipt = uni.w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"Swap mined in block {receipt['blockNumber']}")
make_swap_output() buys an exact output amount. Pass the
quoted input as qtycap; the client applies max_slippage to derive the
maximum input:
# Buy exactly 100 USDC; max_slippage is applied to this quoted ETH input
expected_eth = uni.get_price_output(ETH_ADDRESS, USDC, 100 * 10**6, fee=500, tick_spacing=10)
tx_hash = uni.make_swap_output(
input_token=ETH_ADDRESS,
output_token=USDC,
qty=100 * 10**6,
qtycap=expected_eth,
swap_pool_key=pool_key,
)
Discovering pools#
V4pools scans PoolManager Initialize events to find
all pools for a given pair of tokens.
from web3 import Web3
from uniswap.util import V4pools
from uniswap.constants import ETH_ADDRESS
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))
pools = V4pools(w3)
# Scan the chain (slow on first run — save to disk for reuse)
pools.fetch_poolkey_data(chunk_size=2000)
pools.save_poolkeys_list("/tmp/v4_pools.json")
# Load from disk on subsequent runs
pools.load_poolkeys_list("/tmp/v4_pools.json")
# Get all ETH/USDC pools
eth_usdc_pools = pools.get_poolkeys_sublist(ETH_ADDRESS, USDC)
for pk in eth_usdc_pools:
print(f"fee={pk.fee}, tick_spacing={pk.tick_spacing}, hooks={pk.hooks}")
Managing liquidity#
Uniswap v4 positions are ERC-721 tokens managed by the PositionManager.
Creating a pool#
New pools are created via create_pool(), which sets the
initial price. The price is expressed as sqrtPriceX96 (an integer).
import math
from uniswap.types import PoolKey
from uniswap.constants import ETH_ADDRESS, ZERO_HOOK
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
pool_key = PoolKey(
currency0=ETH_ADDRESS, currency1=USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK
)
# Encode an initial price of ~3400 USDC per ETH as sqrtPriceX96
# price_ratio = token1_amount / token0_amount (in smallest units)
price_ratio = (3400 * 10**6) / 10**18
sqrt_price_x96 = int(math.sqrt(price_ratio) * 2**96)
tx = uni.create_pool(pool_key=pool_key, sqrt_price_x96=sqrt_price_x96)
uni.w3.eth.wait_for_transaction_receipt(tx)
Minting a position#
mint_position() adds liquidity to a tick range and
returns an ERC-721 NFT representing the position.
ONE_ETH = 10**18
tx = uni.mint_position(
pool_key=pool_key,
tick_lower=-887270, # near minimum tick
tick_upper=887270, # near maximum tick (full range)
liquidity=10**18, # amount of liquidity to provide
amount0=ONE_ETH // 10, # max ETH to spend
amount1=340 * 10**6, # max USDC to spend
)
# Retrieve the ERC-721 token ID(s) minted by this transaction
token_ids = uni.get_minted_token_id(tx.hex())
token_id = token_ids[0]
print(f"Minted position with token ID: {token_id}")
Reading position state#
from uniswap.types import PoolKey
position = uni.get_position_info(token_id)
liquidity = uni.position_manager_get_position_liquidity(token_id)
print(f"Liquidity: {liquidity}")
# Read the position value (amounts of token0/token1 at current price)
ETH_DECIMALS = 18
USDC_DECIMALS = 6
value = uni.get_position_value(token_id, ETH_DECIMALS, USDC_DECIMALS)
print(f"token0 amount: {value['amount0']}, token1 amount: {value['amount1']}")
Collecting fees and removing liquidity#
from uniswap.types import PoolKey
# Build the PoolKey from position info
position = uni.get_position_info(token_id)
pool_key = PoolKey(
position["currency0"], position["currency1"],
position["fee"], position["tickSpacing"], position["hooks"]
)
# Collect all accumulated fees
uni.collect_fees(pool_key=pool_key, token_id=token_id)
# Remove all liquidity (burn requires the position to be fully empty)
liquidity = uni.position_manager_get_position_liquidity(token_id)
uni.decrease_liquidity(
pool_key=pool_key,
token_id=token_id,
liquidity=liquidity,
amount0_min=0,
amount1_min=0,
)
# Burn the position NFT once fully emptied
uni.burn_position(pool_key=pool_key, token_id=token_id, amount0_min=0, amount1_min=0)
Reading pool state via StateView#
The StateView contract gives read-only access to pool state without making a swap.
from uniswap.constants import ETH_ADDRESS, ZERO_HOOK
USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
# Current pool price and tick
slot0 = uni.stateview_get_slot0(ETH_ADDRESS, USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK)
print(f"sqrtPriceX96: {slot0['sqrtPriceX96']}")
print(f"current tick: {slot0['tick']}")
# Total in-range liquidity
liquidity = uni.stateview_get_liquidity(ETH_ADDRESS, USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK)
print(f"in-range liquidity: {liquidity}")