Skip to main content

uniswap-v3-expert

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.

Jump to install

Source facts

Repository
ccashwell/evm-cortex
Last source activity
April 10, 2026 at 16:31
Detected SKILL.md language
English
Stars
131
Forks
18

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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)
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub