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:

fee

Rate

Tick spacing

100

0.01%

1

500

0.05%

10

3000

0.30%

60

10000

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}")