- name
- uniswap-v3-expert
- description
- Use when building on, integrating with, or analyzing Uniswap V3. Covers concentrated liquidity, tick-based pricing, UniswapV3Factory, UniswapV3Pool, NonfungiblePositionManager, SwapRouter, oracle observations, fee tiers, and production deployment addresses across all chains.
# Uniswap V3 Expert
## Architecture Overview
Uniswap V3 is a concentrated liquidity AMM where each (tokenA, tokenB, fee) triple gets its own `UniswapV3Pool` contract, deployed via `CREATE2` from a singleton `UniswapV3Factory`. LPs provide liquidity in discrete price ranges instead of across the full (0, infinity) curve, dramatically improving capital efficiency.
### Contract Hierarchy
```
UniswapV3Factory (singleton)
├── creates UniswapV3Pool contracts via CREATE2 (one per token pair + fee tier)
│ ├── Core swap, mint, burn, collect, flash, observe logic
│ └── Stores tick state, positions, observations, and protocol fees
│
Periphery contracts (stateless routers / managers):
├── NonfungiblePositionManager — wraps LP positions as ERC-721 NFTs
├── SwapRouter — single and multi-hop exact-input / exact-output swaps
├── SwapRouter02 — v2+v3 unified router with multicall
├── UniversalRouter — command-based router supporting v2, v3, permits, NFTs
├── Quoter — off-chain swap simulation (reverts internally to return amounts)
├── QuoterV2 — returns sqrtPriceX96After, initializedTicksCrossed, gasEstimate
└── TickLens — batch read initialized ticks for a pool
```
### Token Ordering
Uniswap V3 enforces `token0 < token1` (by address). The factory and pool reject misordered pairs. Always sort before calling factory or pool functions:
```solidity
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
```
### Pool Address Derivation (CREATE2)
Pool addresses are deterministic. You can compute them offchain without querying the factory:
```solidity
address pool = address(uint160(uint256(keccak256(abi.encodePacked(
hex"ff",
factory,
keccak256(abi.encode(token0, token1, fee)),
POOL_INIT_CODE_HASH
)))));
```
The `POOL_INIT_CODE_HASH` for Uniswap V3 is:
`0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54`
## Concentrated Liquidity Model
### Ticks and Price
Price is discretized into **ticks**. The price at tick `i` is:
```
P(i) = 1.0001^i
```
This gives ~1 basis point precision per tick. Ticks range from `MIN_TICK = -887272` to `MAX_TICK = 887272`.
### sqrtPriceX96
Uniswap V3 stores the square root of price as a Q64.96 fixed-point number:
```
sqrtPriceX96 = sqrt(token1 / token0) * 2^96
```
Converting between tick and sqrtPriceX96:
```solidity
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick);
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
```
Boundary values:
- `TickMath.MIN_SQRT_RATIO = 4295128739` (tick -887272)
- `TickMath.MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342` (tick 887272)
### Fee Tiers and Tick Spacing
| Fee (bps) | Fee (uint24) | Tick Spacing | Use Case |
|-----------|-------------|-------------|----------|
| 0.01% | 100 | 1 | Stable pairs (USDC/USDT, DAI/USDC) |
| 0.05% | 500 | 10 | Stable pairs, correlated assets |
| 0.30% | 3000 | 60 | Standard pairs (ETH/USDC, WBTC/ETH) |
| 1.00% | 10000 | 200 | Exotic pairs, high volatility |
Tick spacing means LPs can only place range boundaries at ticks divisible by the spacing. The 1 bps tier was added via governance (not in original deployment).
### Liquidity Math
Within a single tick range, the V3 pool behaves like a constant-product AMM scaled by liquidity `L`:
```
x * y = L^2 (virtual reserves within the active range)
```
Real reserves required for a position between sqrtPriceA and sqrtPriceB with liquidity L:
```
amount0 = L * (1/sqrtPriceA - 1/sqrtPriceB) (when price < lower bound: all token0)
amount1 = L * (sqrtPriceB - sqrtPriceA) (when price > upper bound: all token1)
```
When price is within the range, the position holds a mix of both tokens.
## Core Contract Functions
### UniswapV3Factory
```solidity
interface IUniswapV3Factory {
/// @notice Creates a pool for the given two tokens and fee
/// @param tokenA One of the two tokens in the desired pool
/// @param tokenB The other of the two tokens in the desired pool
/// @param fee The desired fee for the pool (100, 500, 3000, or 10000)
/// @return pool The address of the newly created pool
function createPool(
address tokenA,
address tokenB,
uint24 fee
) external returns (address pool);
/// @notice Returns the pool address for a given pair of tokens and fee, or address(0)
function getPool(
address tokenA,
address tokenB,
uint24 fee
) external view returns (address pool);
/// @notice Returns the tick spacing for a given fee amount
function feeAmountTickSpacing(uint24 fee) external view returns (int24);
/// @notice Returns the current protocol fee controller
function owner() external view returns (address);
/// @notice Enables a fee amount with the given tick spacing (governance only)
function enableFeeAmount(uint24 fee, int24 tickSpacing) external;
}
```
### UniswapV3Pool
```solidity
interface IUniswapV3Pool {
/// @notice Sets the initial price for the pool. Can only be called once.
/// @param sqrtPriceX96 The initial sqrt price as a Q64.96 value
function initialize(uint160 sqrtPriceX96) external;
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position
/// @dev The caller of this method receives a callback (uniswapV3MintCallback)
/// in which they must pay any token0 or token1 owed for the liquidity
/// @param recipient The address for which the liquidity will be created
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param amount The amount of liquidity to mint
/// @param data Any data to be passed through to the callback
/// @return amount0 The amount of token0 that was paid to mint
/// @return amount1 The amount of token1 that was paid to mint
function mint(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount,
bytes calldata data
) external returns (uint256 amount0, uint256 amount1);
/// @notice Burns liquidity from the sender and accounts tokens owed
/// @dev Does NOT transfer tokens — must call collect() afterward
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param amount The amount of liquidity to burn
/// @return amount0 The amount of token0 owed to the position
/// @return amount1 The amount of token1 owed to the position
function burn(
int24 tickLower,
int24 tickUpper,
uint128 amount
) external returns (uint256 amount0, uint256 amount1);
/// @notice Collects tokens owed to a position
/// @dev Must burn(0) first to update fee accounting if only collecting fees
/// @param recipient The address which should receive the collected tokens
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param amount0Requested How much token0 should be withdrawn
/// @param amount1Requested How much token1 should be withdrawn
/// @return amount0 The amount of token0 collected
/// @return amount1 The amount of token1 collected
function collect(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount0Requested,
uint128 amount1Requested
) external returns (uint128 amount0, uint128 amount1);
/// @notice Swap token0 for token1, or token1 for token0
/// @param recipient The address to receive the output of the swap
/// @param zeroForOne Direction: true = token0 → token1, false = token1 → token0
/// @param amountSpecified Positive = exact input, negative = exact output
/// @param sqrtPriceLimitX96 Price limit — swap stops if crossed
/// For zeroForOne: must be < current price and > MIN_SQRT_RATIO
/// For oneForZero: must be > current price and < MAX_SQRT_RATIO
/// @param data Callback data passed to uniswapV3SwapCallback
/// @return amount0 Delta of token0 balance of the pool (positive = pool received)
/// @return amount1 Delta of token1 balance of the pool
function swap(
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external returns (int256 amount0, int256 amount1);
/// @notice Flash loans both tokens
/// @param recipient The address which will receive the token0 and token1 amounts
/// @param amount0 The amount of token0 to flash
/// @param amount1 The amount of token1 to flash
/// @param data Callback data passed to uniswapV3FlashCallback
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
/// @notice Returns cumulative tick and liquidity values at given seconds ago
/// @param secondsAgos Array of seconds ago from current block timestamp
/// @return tickCumulatives Cumulative tick values at each secondsAgo
/// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity
function observe(
uint32[] calldata secondsAgos
) external view returns (
int56[] memory tickCumulatives,
uint160[] memory secondsPerLiquidityCumulativeX128s
);
/// @notice Grows the observation buffer to support longer TWAPs
/// @param observationCardinalityNext Minimum number of observations to store
function increaseObservationCardinalityNext(
uint16 observationCardinalityNext
) external;
// --- State view functions ---
function slot0() external view returns (
uint160 sqrtPriceX96,
int24 tick,
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint8 feeProtocol,
bool unlocked
);
function liquidity() external view returns (uint128);
function fee() external view returns (uint24);
function token0() external view returns (address);
function token1() external view returns (address);
function tickSpacing() external view returns (int24);
function maxLiquidityPerTick() external view returns (uint128);
function ticks(int24 tick) external view returns (
uint128 liquidityGross,
int128 liquidityNet,
uint256 feeGrowthOutside0X128,
uint256 feeGrowthOutside1X128,
int56 tickCumulativeOutside,
uint160 secondsPerLiquidityOutsideX128,
uint32 secondsOutside,
bool initialized
);
function positions(bytes32 key) external view returns (
uint128 _liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
}
```
Position keys for the core pool contract use `keccak256(abi.encodePacked(owner, tickLower, tickUpper))`.
## NonfungiblePositionManager
The `NonfungiblePositionManager` (NPM) wraps core pool positions as ERC-721 NFTs. Most LPs interact with V3 through the NPM rather than calling the pool directly.
```solidity
interface INonfungiblePositionManager {
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
/// @notice Creates a new position wrapped in an NFT
/// @return tokenId The ID of the minted NFT
/// @return liquidity The amount of liquidity for this position
/// @return amount0 The amount of token0 deposited
/// @return amount1 The amount of token1 deposited
function mint(MintParams calldata params)
external
payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
struct IncreaseLiquidityParams {
uint256 tokenId;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function increaseLiquidity(IncreaseLiquidityParams calldata params)
external
payable
returns (uint128 liquidity, uint256 amount0, uint256 amount1);
struct DecreaseLiquidityParams {
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
external
payable
returns (uint256 amount0, uint256 amount1);
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
/// @notice Collects fees and principal owed to a position
function collect(CollectParams calldata params)
external
payable
returns (uint256 amount0, uint256 amount1);
/// @notice Burns a token ID. Position must have 0 liquidity and 0 tokens owed.
function burn(uint256 tokenId) external payable;
/// @notice Returns the position data for a given token ID
function positions(uint256 tokenId) external view returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
}
```
### Collecting Fees
To collect accrued trading fees without removing liquidity, call `decreaseLiquidity` with `liquidity = 0` (or simply use `collect` after calling `burn(0)` on the core pool). Through the NPM:
```solidity
// Trigger fee accounting update via zero-amount decrease
positionManager.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: 0,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
}));
// Collect all owed tokens (fees + any burned principal)
positionManager.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
```
### Full Position Lifecycle
```solidity
// 1. Approve tokens to NPM
IERC20(token0).approve(address(positionManager), amount0);
IERC20(token1).approve(address(positionManager), amount1);
// 2. Mint position
(uint256 tokenId, uint128 liquidity, uint256 used0, uint256 used1) =
positionManager.mint(INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: 3000,
tickLower: -60,
tickUpper: 60,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0,
amount1Min: 0,
recipient: msg.sender,
deadline: block.timestamp
}));
// 3. Collect fees (anytime)
positionManager.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
// 4. Remove liquidity
positionManager.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
}));
// 5. Collect principal + remaining fees
positionManager.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
// 6. Burn NFT (optional, position must have 0 liquidity and 0 owed)
positionManager.burn(tokenId);
```
## SwapRouter Integration
### SwapRouter (original)
```solidity
interface ISwapRouter {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96; // 0 for no limit
}
/// @notice Swaps amountIn of one token for as much as possible of another token
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);
struct ExactInputParams {
bytes path; // abi.encodePacked(tokenIn, fee, ..., tokenOut)
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
}
/// @notice Swaps along the specified multi-hop path
function exactInput(ExactInputParams calldata params)
external
payable
returns (uint256 amountOut);
struct ExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountOut;
uint256 amountInMaximum;
uint160 sqrtPriceLimitX96;
}
/// @notice Swaps as little as possible of one token for amountOut of another
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
returns (uint256 amountIn);
struct ExactOutputParams {
bytes path; // abi.encodePacked(tokenOut, fee, ..., tokenIn) — REVERSED
address recipient;
uint256 deadline;
uint256 amountOut;
uint256 amountInMaximum;
}
/// @notice Swaps along a reversed path to get exact output amount
function exactOutput(ExactOutputParams calldata params)
external
payable
returns (uint256 amountIn);
}
```
### Multi-Hop Path Encoding
Paths are tightly packed sequences of `(token, fee, token, fee, ..., token)`:
```solidity
// Single hop: WETH → 0.3% → USDC
bytes memory path = abi.encodePacked(WETH, uint24(3000), USDC);
// Multi-hop: WETH → 0.3% → USDC → 0.01% → DAI
bytes memory path = abi.encodePacked(WETH, uint24(3000), USDC, uint24(100), DAI);
```
For `exactOutput`, the path is **reversed** (starts with output token):
```solidity
// Exact output multi-hop: want DAI, pay WETH
// Path is: DAI → 0.01% → USDC → 0.3% → WETH (reversed order)
bytes memory path = abi.encodePacked(DAI, uint24(100), USDC, uint24(3000), WETH);
```
### SwapRouter02 (Unified V2+V3 Router)
Auf GitHub ansehen