Skip to main content

uniswap-v4-expert

Use when building on, integrating with, or analyzing Uniswap V4. Covers PoolManager singleton architecture, flash accounting via EIP-1153 transient storage, hook lifecycle, PoolKey structure, Currency type, dynamic fees, custom accounting, native ETH support, PositionManager (ERC-721 positions), and production deployment addresses.

Informations de source

Dépôt
ccashwell/evm-cortex
Dernière activité de la source
29 septembre 2026 à 14:14
Langue détectée de SKILL.md
anglais
Étoiles
131
Forks
18

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
uniswap-v4-expert
description
Use when building on, integrating with, or analyzing Uniswap V4. Covers PoolManager singleton architecture, flash accounting via EIP-1153 transient storage, hook lifecycle, PoolKey structure, Currency type, dynamic fees, custom accounting, native ETH support, PositionManager (ERC-721 positions), and production deployment addresses.
# Uniswap V4 Expert ## Architecture Overview Uniswap V4 replaces V3's factory-per-pool model with a **singleton PoolManager** — every pool lives inside a single contract. This eliminates redundant bytecode deployments and enables multi-hop swaps to settle only net token transfers. All state-changing operations use **flash accounting** via EIP-1153 transient storage: callers accumulate deltas during an `unlock()` callback and must zero out all balances before the callback returns. ### Singleton Design ``` ┌────────────────────────────────────────────┐ │ PoolManager │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ │ │ │ Pool A │ │ Pool B │ │ Pool C │ │ │ │ ETH/USDC │ │ WBTC/USDC │ │ ETH/DAI │ │ │ └──────────┘ └───────────┘ └──────────┘ │ │ │ │ Transient Storage (EIP-1153) │ │ ┌──────────────────────────────────────┐ │ │ │ currency → delta mapping (per lock) │ │ │ └──────────────────────────────────────┘ │ └────────────────────────────────────────────┘ ``` ### Flash Accounting Flow 1. Caller invokes `poolManager.unlock(data)` 2. PoolManager calls `IUnlockCallback(msg.sender).unlockCallback(data)` 3. Inside the callback, caller executes operations (swap, modifyLiquidity, donate) 4. Each operation updates transient storage deltas — no token transfers yet 5. Caller resolves deltas via `settle()` (pay tokens in) and `take()` (withdraw tokens out) 6. On return from `unlockCallback`, PoolManager verifies all currency deltas are zero 7. If any delta is nonzero, the transaction reverts with `CurrencyNotSettled()` This means multi-hop swaps (e.g., A→B→C) only require net token movements for A and C, saving gas on intermediate transfers. ### Functions Callable Outside unlock() Functions callable outside `unlock()`: - `initialize()` — creates a new pool (no balance changes) - `sync(currency)` — snapshots reserves into transient storage; harmless outside a lock - `updateDynamicLPFee()` — called by hook contracts to set the current dynamic fee - the `IProtocolFees` admin functions (`setProtocolFeeController`, `setProtocolFee`, `collectProtocolFees`) `swap`, `modifyLiquidity`, `donate`, `take`, `settle`, `settleFor`, `clear`, `mint`, `burn` are all `onlyWhenUnlocked` and must run inside an active `unlockCallback`. ## Core Types ### PoolKey The unique identifier for a pool. Defined in `v4-core/src/types/PoolKey.sol`: ```solidity import {Currency} from "v4-core/src/types/Currency.sol"; import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; struct PoolKey { /// @notice The lower currency of the pool, sorted numerically Currency currency0; /// @notice The higher currency of the pool, sorted numerically Currency currency1; /// @notice The pool LP fee, capped at 1_000_000. If the highest bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000 uint24 fee; /// @notice Ticks that involve positions must be a multiple of tick spacing int24 tickSpacing; /// @notice The hooks of the pool IHooks hooks; } ``` **Sorting invariant**: `currency0 < currency1` is enforced. The PoolManager reverts with `CurrenciesOutOfOrderOrEqual` if violated. When constructing a PoolKey, always sort currencies by address value. ### PoolId A `bytes32` hash of the PoolKey, used as the storage key for pool state. Defined in `v4-core/src/types/PoolId.sol`: ```solidity type PoolId is bytes32; library PoolIdLibrary { function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) { assembly ("memory-safe") { // 0xa0 = 5 slots × 32 bytes (total size of PoolKey struct) poolId := keccak256(poolKey, 0xa0) } } } ``` Usage: `using PoolIdLibrary for PoolKey;` then `key.toId()`. ### Currency An address wrapper where `address(0)` represents native ETH. Defined in `v4-core/src/types/Currency.sol`: ```solidity type Currency is address; library CurrencyLibrary { Currency public constant ADDRESS_ZERO = Currency.wrap(address(0)); function isAddressZero(Currency currency) internal pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(ADDRESS_ZERO); } function transfer(Currency currency, address to, uint256 amount) internal { /* handles ETH vs ERC-20 */ } function balanceOfSelf(Currency currency) internal view returns (uint256) { /* handles ETH vs ERC-20 */ } } ``` Native ETH pools use `Currency.wrap(address(0))` as one of the currencies. No WETH wrapping required. ### BalanceDelta Two `int128` values packed into a single `int256`. Upper 128 bits = amount0, lower 128 bits = amount1. Defined in `v4-core/src/types/BalanceDelta.sol`: ```solidity type BalanceDelta is int256; library BalanceDeltaLibrary { BalanceDelta public constant ZERO_DELTA = BalanceDelta.wrap(0); function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) { assembly ("memory-safe") { _amount0 := sar(128, balanceDelta) } } function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) { assembly ("memory-safe") { _amount1 := signextend(15, balanceDelta) } } } ``` Delta semantics from the caller's perspective: - **Negative** delta = caller owes tokens to PoolManager (must `settle()`) - **Positive** delta = PoolManager owes tokens to caller (can `take()`) ### BeforeSwapDelta Return type of the `beforeSwap` hook. Upper 128 bits = delta in **specified** tokens, lower 128 bits = delta in **unspecified** tokens. Defined in `v4-core/src/types/BeforeSwapDelta.sol`: ```solidity type BeforeSwapDelta is int256; function toBeforeSwapDelta(int128 deltaSpecified, int128 deltaUnspecified) pure returns (BeforeSwapDelta beforeSwapDelta) { assembly ("memory-safe") { beforeSwapDelta := or(shl(128, deltaSpecified), and(sub(shl(128, 1), 1), deltaUnspecified)) } } library BeforeSwapDeltaLibrary { BeforeSwapDelta public constant ZERO_DELTA = BeforeSwapDelta.wrap(0); function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128); function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128); } ``` ## PoolManager Interface Full interface from `v4-core/src/interfaces/IPoolManager.sol`. The PoolManager inherits `IProtocolFees`, `IERC6909Claims`, `IExtsload`, and `IExttload`. ### initialize ```solidity function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick); ``` Creates a new pool. Does NOT require the unlock context. Reverts if `currency0 >= currency1`, if `tickSpacing` is zero or exceeds `type(int16).max`, or if the pool already exists. Emits `Initialize` event. ### unlock ```solidity function unlock(bytes calldata data) external returns (bytes memory); ``` Entry point for all delta-accounting operations. Calls `IUnlockCallback(msg.sender).unlockCallback(data)`. After the callback returns, asserts all currency deltas are zero. ### swap ```solidity function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) external returns (BalanceDelta swapDelta); ``` Executes a swap. Only callable inside `unlockCallback`. Invokes `beforeSwap` and `afterSwap` hooks if the pool's hook contract has those permissions. ### modifyLiquidity ```solidity function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData) external returns (BalanceDelta callerDelta, BalanceDelta feesAccrued); ``` Adds or removes liquidity. Returns both the principal delta and fees accrued. A zero `liquidityDelta` "pokes" the position to collect fees without changing liquidity. ### donate ```solidity function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) external returns (BalanceDelta); ``` Distributes tokens to in-range liquidity providers. Useful for hook-driven fee distribution or protocol reward injection. ### Settlement Functions ```solidity function settle() external payable returns (uint256 paid); function settleFor(address recipient) external payable returns (uint256 paid); function sync(Currency currency) external; function take(Currency currency, address to, uint256 amount) external; function clear(Currency currency, uint256 amount) external; ``` **settle()**: Pays what the caller owes. For ERC-20 tokens, the caller must first call `sync(currency)`, transfer tokens to the PoolManager, then call `settle()`. For native ETH, send value directly with `settle{value: amount}()`. Returns the amount credited. **sync(currency)**: Snapshots the PoolManager's current ERC-20 balance into transient storage. MUST be called before transferring ERC-20 tokens for settlement. Not needed for native ETH. **take(currency, to, amount)**: Withdraws tokens the PoolManager owes to the caller. Reverts if the caller's delta for that currency is insufficient. **clear(currency, amount)**: Zeros out a positive delta WITHOUT transferring tokens. The tokens are permanently locked in the PoolManager. Use only for dust amounts. ### ERC-6909 Claims ```solidity function mint(address to, uint256 id, uint256 amount) external; function burn(address from, uint256 id, uint256 amount) external; ``` Converts currency deltas into ERC-6909 claim tokens (and vice versa). The `id` is the currency address cast to `uint256`. Useful for holding balances inside the PoolManager across transactions without actual token transfers. ## Parameter Structs ### SwapParams Defined in `v4-core/src/types/PoolOperation.sol`: ```solidity struct SwapParams { /// Whether to swap token0 for token1 or vice versa bool zeroForOne; /// The desired input amount if negative (exactIn), or the desired output amount if positive (exactOut) int256 amountSpecified; /// The sqrt price at which, if reached, the swap will stop executing uint160 sqrtPriceLimitX96; } ``` **CRITICAL**: `amountSpecified` sign convention: - **Negative** = exact input (caller specifies how much to spend) - **Positive** = exact output (caller specifies how much to receive) Price limits: - `zeroForOne = true`: set `sqrtPriceLimitX96` to a value **less than** the current price (price decreases) - `zeroForOne = false`: set `sqrtPriceLimitX96` to a value **greater than** the current price (price increases) - Use `TickMath.MIN_SQRT_PRICE + 1` or `TickMath.MAX_SQRT_PRICE - 1` for unlimited slippage ### ModifyLiquidityParams Defined in `v4-core/src/types/PoolOperation.sol`: ```solidity struct ModifyLiquidityParams { int24 tickLower; int24 tickUpper; int256 liquidityDelta; bytes32 salt; } ``` - `liquidityDelta > 0`: add liquidity - `liquidityDelta < 0`: remove liquidity - `liquidityDelta == 0`: poke (collect accrued fees only) - `salt`: differentiates multiple positions at the same tick range from the same address ## Fee System ### Static Fees Set at pool creation via `PoolKey.fee`. Denominated in **hundredths of a basis point** (1/100th of 1/10000th): | PoolKey.fee | Effective Fee | |-------------|--------------| | 100 | 0.01% | | 500 | 0.05% | | 3000 | 0.30% | | 10000 | 1.00% | | 1000000 | 100% (MAX) | ### Dynamic Fees From `v4-core/src/libraries/LPFeeLibrary.sol`: ```solidity library LPFeeLibrary { uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; uint24 public constant OVERRIDE_FEE_FLAG = 0x400000; uint24 public constant REMOVE_OVERRIDE_MASK = 0xBFFFFF; uint24 public constant MAX_LP_FEE = 1000000; // 100% } ``` To create a dynamic fee pool, set `PoolKey.fee = LPFeeLibrary.DYNAMIC_FEE_FLAG` (exactly `0x800000`). Two mechanisms for dynamic fee updates: 1. **Persistent update**: Hook calls `poolManager.updateDynamicLPFee(key, newFee)` (e.g., in `afterInitialize` or periodically). This sets the stored fee for subsequent swaps. 2. **Per-swap override**: `beforeSwap` returns a fee with the override flag set in the third return value (`uint24`). The returned fee is `desiredFee | LPFeeLibrary.OVERRIDE_FEE_FLAG`. This overrides the stored fee for that single swap only. ```solidity import {SwapParams} from "v4-core/src/types/PoolOperation.sol"; // Inside a BaseHook subclass: override the internal _beforeSwap, not the external entry point function _beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata) internal override returns (bytes4, BeforeSwapDelta, uint24) { uint24 dynamicFee = _computeFee(); return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, dynamicFee | LPFeeLibrary.OVERRIDE_FEE_FLAG); } ``` ### Protocol Fees Set by the `protocolFeeController` (appointed by the PoolManager owner via `setProtocolFeeController`) through `IProtocolFees.setProtocolFee(PoolKey, uint24)`. The `uint24` packs two direction-specific fees in pips: lower 12 bits = zeroForOne, upper 12 bits = oneForZero, each `<= ProtocolFeeLibrary.MAX_PROTOCOL_FEE` (1000 pips = 0.1%). The protocol fee is charged on the swap input first; the LP fee applies to the remainder (`swapFee = protocolFee + lpFee - protocolFee * lpFee / 1e6`). Protocol fees are live on mainnet V4 pools since the governance proposal "Activate v4 Protocol Fees (Part 1/2)" executed on 2026-07-27. On Ethereum, `PoolManager.owner()` is the governance timelock `0x1a9C8182C09F50C8318d769245beA52c32BE35BC` and `protocolFeeController()` is `0x89A5D5bF00a27D55c02951E49078a5C5771051dB`; the native ETH/USDC 500/10 pool reports `protocolFee = 512125` (125 pips in each direction). Fee-revenue models for hooks and LPs must account for this cut. ## PositionManager (Periphery) The `PositionManager` is the canonical periphery contract for managing liquidity positions as ERC-721 NFTs. Source: `v4-periphery/src/PositionManager.sol`. ```solidity contract PositionManager is IPositionManager, ERC721Permit_v4, // ERC-721 + EIP-4494 permit PoolInitializer_v4, Multicall_v4, DeltaResolver, ReentrancyLock, BaseActionsRouter, // action dispatch via unlock Notifier, // subscriber/notification pattern Permit2Forwarder, // Permit2 integration NativeWrapper // WETH wrapping/unwrapping { ... } ``` **NFT metadata**: Name = `"Uniswap v4 Positions NFT"`, Symbol = `"UNI-V4-POSM"`. ### Entry Point ```solidity function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable; ``` The standard entry point. Encodes a sequence of actions and their parameters. The `unlockData` is ABI-encoded as `(bytes actions, bytes[] params)` where `actions` is a packed byte array of action codes. ### Action Codes From `v4-periphery/src/libraries/Actions.sol`: ```solidity library Actions { uint256 internal constant INCREASE_LIQUIDITY = 0x00; uint256 internal constant DECREASE_LIQUIDITY = 0x01; uint256 internal constant MINT_POSITION = 0x02; uint256 internal constant BURN_POSITION = 0x03; uint256 internal constant SWAP_EXACT_IN_SINGLE = 0x06; uint256 internal constant SWAP_EXACT_IN = 0x07; uint256 internal constant SWAP_EXACT_OUT_SINGLE = 0x08; uint256 internal constant SWAP_EXACT_OUT = 0x09; uint256 internal constant DONATE = 0x0a; // not supported by PositionManager or V4Router uint256 internal constant SETTLE = 0x0b; uint256 internal constant SETTLE_ALL = 0x0c; uint256 internal constant SETTLE_PAIR = 0x0d; uint256 internal constant TAKE = 0x0e; uint256 internal constant TAKE_ALL = 0x0f; uint256 internal constant TAKE_PORTION = 0x10; uint256 internal constant TAKE_PAIR = 0x11; uint256 internal constant CLOSE_CURRENCY = 0x12; uint256 internal constant CLEAR_OR_TAKE = 0x13; uint256 internal constant SWEEP = 0x14; uint256 internal constant WRAP = 0x15; uint256 internal constant UNWRAP = 0x16; uint256 internal constant MINT_6909 = 0x17; // not supported by PositionManager or V4Router uint256 internal constant BURN_6909 = 0x18; // not supported by PositionManager or V4Router uint256 internal constant UNWIND_WITH_FALLBACK = 0x19; } ``` The library defines 26 constants (`0x00`–`0x19`); the two deprecated `*_FROM_DELTAS` codes are listed below. **DEPRECATED** (vulnerable to sandwich attacks — lack slippage protection): - `INCREASE_LIQUIDITY_FROM_DELTAS` (0x04) - `MINT_POSITION_FROM_DELTAS` (0x05) ### Typical Action Sequences Mint a new position: ``` [MINT_POSITION, SETTLE_PAIR, SWEEP] // or CLOSE_CURRENCY for each ``` Increase liquidity on existing position: ``` [INCREASE_LIQUIDITY, SETTLE_PAIR, SWEEP] ```
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub