---
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]
```

Decrease liquidity and collect:
```
[DECREASE_LIQUIDITY, TAKE_PAIR]
```

Burn an empty position:
```
[BURN_POSITION]  // position must have zero liquidity
```

### Subscriber/Notification Pattern

The `Notifier` base enables position subscribers — external contracts that receive callbacks when a position is modified. Subscribers implement `ISubscriber`:

```solidity
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {PositionInfo} from "v4-periphery/src/libraries/PositionInfoLibrary.sol";

interface ISubscriber {
    function notifySubscribe(uint256 tokenId, bytes memory data) external;
    function notifyUnsubscribe(uint256 tokenId) external;
    function notifyModifyLiquidity(uint256 tokenId, int256 liquidityChange, BalanceDelta feesAccrued) external;
    function notifyBurn(uint256 tokenId, address owner, PositionInfo info, uint256 liquidity, BalanceDelta feesAccrued) external;
}
```

Subscribe via `positionManager.subscribe(tokenId, subscriber, data)`. The subscriber is notified on every liquidity modification or burn.

## Production Deployment Addresses

### Ethereum Mainnet (Chain ID: 1)

| Contract | Address |
|----------|---------|
| PoolManager | `0x000000000004444c5dc75cB358380D2e3dE08A90` |
| Universal Router (V2) | `0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af` |
| Universal Router 2.1.1 | `0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA` |
| Universal Router 2.1.2 | `0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85` |
| PositionManager | `0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e` |
| PositionDescriptor | `0xd1428Ba554F4C8450b763a0B2040A4935c63f06C` |
| StateView | `0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227` |
| V4Quoter | `0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203` |
| ReservesLens | `0x0000001b173C3bbF3984D417d8614E3eed34865B` |
| Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` |

### Unichain (Chain ID: 130)

| Contract | Address |
|----------|---------|
| PoolManager | `0x1F98400000000000000000000000000000000004` |
| PositionManager | `0x4529A01c7A0410167c5740C487A8DE60232617bf` |
| StateView | `0x86e8631A016F9068C3f085fAF484Ee3F5fDee8f2` |
| V4Quoter | `0x333E3C607B141b18fF6de9f258db6e77fE7491E0` |
| Universal Router (V2) | `0xEf740bf23aCaE26f6492B10de645D6B98dC8Eaf3` |
| Universal Router 2.1.2 | `0xD1b797D92d87B688193A2B976eFc8D577D204343` |

RPC: `https://mainnet.unichain.org`. All Unichain addresses above were verified with `cast code`; the Ethereum PoolManager address has no code on Unichain. Unichain's `protocolFeeController()` is still `address(0)`, so protocol fees are not yet active there.

### Supported Chains

V4 is deployed on 19 mainnets: **Ethereum, Unichain, Optimism, Base, Arbitrum One, Polygon, Zora, Worldchain, X Layer, Ink, Soneium, Avalanche, BNB Smart Chain, Celo, Monad, MegaETH, Tempo, Robinhood Chain, Arc**. Blast was removed from the official list (its onchain status is unverified).

**CRITICAL**: Addresses are NOT the same across chains. Always verify per-chain at https://developers.uniswap.org/docs/protocols/v4/deployments. Use `cast code <address> --rpc-url <rpc>` to confirm deployment before integrating.

## Integration Patterns

### Custom Router (unlockCallback)

A minimal router that performs a swap by implementing `IUnlockCallback`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {SwapParams} from "v4-core/src/types/PoolOperation.sol";
import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol";
import {IERC20Minimal} from "v4-core/src/interfaces/external/IERC20Minimal.sol";

contract SimpleSwapRouter is IUnlockCallback {
    using CurrencyLibrary for Currency;
    using TransientStateLibrary for IPoolManager;

    IPoolManager public immutable poolManager;

    constructor(IPoolManager _poolManager) {
        poolManager = _poolManager;
    }

    function swap(PoolKey calldata key, SwapParams calldata params) external payable {
        poolManager.unlock(abi.encode(key, params, msg.sender));
    }

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert();

        (PoolKey memory key, SwapParams memory params, address sender) =
            abi.decode(data, (PoolKey, SwapParams, address));

        BalanceDelta delta = poolManager.swap(key, params, "");

        _settleDelta(sender, key.currency0, delta.amount0());
        _settleDelta(sender, key.currency1, delta.amount1());

        return "";
    }

    function _settleDelta(address sender, Currency currency, int128 delta) internal {
        if (delta < 0) {
            // Caller owes tokens to PoolManager
            uint256 amount = uint256(uint128(-delta));
            if (currency.isAddressZero()) {
                poolManager.settle{value: amount}();
            } else {
                poolManager.sync(currency);
                IERC20Minimal(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), amount);
                poolManager.settle();
            }
        } else if (delta > 0) {
            // PoolManager owes tokens to caller
            poolManager.take(currency, sender, uint256(uint128(delta)));
        }
    }
}
```

### Settle/Take Pattern Summary

```
ERC-20 settlement:
  1. poolManager.sync(currency)           — snapshot current balance
  2. token.transferFrom(user, poolManager) — transfer tokens in
  3. poolManager.settle()                  — credit the delta

Native ETH settlement:
  1. poolManager.settle{value: amount}()   — send ETH directly

Withdrawal:
  1. poolManager.take(currency, recipient, amount)
```

### ERC-6909 Claim Token Pattern

For protocols that hold persistent balances in the PoolManager (avoiding repeated transfers):

```solidity
// Convert positive delta to ERC-6909 claim tokens (keep balance in PM)
poolManager.mint(address(this), currency.toId(), amount);

// Later, burn claim tokens to create a negative delta (as if depositing)
poolManager.burn(address(this), currency.toId(), amount);
```

## Foundry Setup

### Installation

```bash
forge install uniswap/v4-core
forge install uniswap/v4-periphery
forge install OpenZeppelin/uniswap-hooks   # BaseHook (no longer in v4-periphery)
```

### Remappings (foundry.toml or remappings.txt)

```toml
[profile.default]
remappings = [
    "v4-core/=lib/v4-core/",
    "v4-periphery/=lib/v4-periphery/",
    "@uniswap/v4-core/=lib/v4-core/",
    "@uniswap/v4-periphery/=lib/v4-periphery/",
    "@openzeppelin/uniswap-hooks/=lib/uniswap-hooks/",
    "permit2/=lib/v4-periphery/lib/permit2/",
    "forge-std/=lib/forge-std/src/",
]
```

### Import Paths

```solidity
// Core types
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol";

// Core interfaces
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";

// Libraries
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol";

// Periphery — hooks (BaseHook no longer ships in v4-periphery; pick one)
import {BaseHook} from "@openzeppelin/uniswap-hooks/src/base/BaseHook.sol";   // v4-template default
// or: import {BaseHook} from "v4-hooks-public/src/base/BaseHook.sol";       // Uniswap Labs

// Periphery — position management
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";
```

### Test Harness

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Test} from "forge-std/Test.sol";
import {Deployers} from "v4-core/test/utils/Deployers.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";

contract MyV4Test is Test, Deployers {
    function setUp() public {
        deployFreshManagerAndRouters();
        deployMintAndApprove2Currencies();

        (key,) = initPool(
            currency0,
            currency1,
            IHooks(address(0)),  // no hook
            3000,                // 0.30% fee
            SQRT_PRICE_1_1       // 1:1 starting price
        );
    }
}
```

The `Deployers` helper from `v4-core/test/utils/Deployers.sol` provides `deployFreshManagerAndRouters()`, `deployMintAndApprove2Currencies()`, `initPool()`, and test routers (`swapRouter`, `modifyLiquidityRouter`).

## Key Differences from V3

| Aspect | Uniswap V3 | Uniswap V4 |
|--------|-----------|-----------|
| Architecture | Factory + individual pool contracts | Singleton PoolManager |
| Pool identification | Contract address | `PoolKey` → `PoolId` (bytes32 hash) |
| Token transfers | Direct transfers on every operation | Flash accounting (deltas in transient storage) |
| Multi-hop efficiency | Transfer tokens between each pool | Net settlement — only endpoints transfer |
| Native ETH | Must wrap to WETH first | Native ETH via `Currency.wrap(address(0))` |
| Extensibility | No hook system | 10 hook callbacks (14 permission flags incl. 4 return-delta flags) |
| Fee model | Fixed fee tiers (0.01%, 0.05%, 0.30%, 1%) | Arbitrary static fees + dynamic fees via hooks |
| Fee distribution | Swap fees only | `donate()` for direct distribution to LPs |
| Position NFTs | NonfungiblePositionManager (V3) | PositionManager (still ERC-721) with Permit2 and batched `Actions`; ERC-6909 is for PoolManager claim tokens, not positions |
| LP fee updates | Immutable after pool creation | Dynamic via `updateDynamicLPFee()` |
| Transient storage | Not used (pre-Cancun) | EIP-1153 for delta tracking |
| Flash loans | Dedicated `flash()` function | Implicit via unlock — take first, settle later |
| Custom accounting | Not possible | Hooks can modify swap amounts via return deltas |
| Solidity version | 0.7.6 | 0.8.26 |

### Flash Loans in V4

V4 has no dedicated flash loan function. Flash loans are implicit: inside `unlockCallback`, call `take()` to receive tokens, use them, then `settle()` to repay. As long as all deltas net to zero before the callback returns, the transaction succeeds. Effectively zero-fee flash loans.

```solidity
function unlockCallback(bytes calldata) external returns (bytes memory) {
    // Borrow 1000 USDC
    poolManager.take(usdc, address(this), 1000e6);

    // ... use the USDC (arbitrage, liquidation, etc.) ...

    // Repay 1000 USDC
    poolManager.sync(usdc);
    IERC20(Currency.unwrap(usdc)).transfer(address(poolManager), 1000e6);
    poolManager.settle();

    return "";
}
```

## Hook System Reference

### Permission Flags

Hook addresses encode permissions in the leading bits of the address. The PoolManager validates these at pool initialization.

```solidity
struct Permissions {
    bool beforeInitialize;
    bool afterInitialize;
    bool beforeAddLiquidity;
    bool afterAddLiquidity;
    bool beforeRemoveLiquidity;
    bool afterRemoveLiquidity;
    bool beforeSwap;
    bool afterSwap;
    bool beforeDonate;
    bool afterDonate;
    bool beforeSwapReturnDelta;
    bool afterSwapReturnDelta;
    bool afterAddLiquidityReturnDelta;
    bool afterRemoveLiquidityReturnDelta;
}
```

### Hook Callback Signatures

```solidity
function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96)
    external returns (bytes4);

function afterInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, int24 tick)
    external returns (bytes4);

function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, bytes calldata hookData)
    external returns (bytes4);

function afterAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData)
    external returns (bytes4, BalanceDelta);

function beforeRemoveLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, bytes calldata hookData)
    external returns (bytes4);

function afterRemoveLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData)
    external returns (bytes4, BalanceDelta);

function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
    external returns (bytes4, BeforeSwapDelta, uint24);

function afterSwap(address sender, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata hookData)
    external returns (bytes4, int128);

function beforeDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1, bytes calldata hookData)
    external returns (bytes4);

function afterDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1, bytes calldata hookData)
    external returns (bytes4);
```

### Return Delta Hooks

When a hook has `beforeSwapReturnDelta` permission, the `BeforeSwapDelta` it returns modifies the swap:
- `deltaSpecified` (upper 128 bits): adjusts the specified token amount
- `deltaUnspecified` (lower 128 bits): adjusts the unspecified token amount

When a hook has `afterSwapReturnDelta` permission, the `int128` returned from `afterSwap` modifies the unspecified token delta.

## Common Pitfalls

1. **Forgetting sync() before ERC-20 settlement**: The PoolManager calculates payment by diffing its balance before and after. Without `sync()`, the diff is wrong.

2. **amountSpecified sign confusion**: Negative = exact input, positive = exact output. This is the reverse of what many developers expect.

3. **Currency sorting**: `currency0 < currency1` is mandatory. Sort by address value before constructing `PoolKey`.

4. **sqrtPriceLimitX96 direction**: For `zeroForOne = true`, the limit must be BELOW current price. For `zeroForOne = false`, ABOVE. Using the wrong direction causes silent no-ops or reverts.

5. **Hook address mismatch**: Hook permission bits are encoded in the address itself. A hook deployed to the wrong address will fail validation at pool initialization.

6. **Unchecked delta resolution**: Every positive and negative delta MUST be resolved before `unlockCallback` returns. Partial resolution causes `CurrencyNotSettled()` revert.

7. **Reentrancy through unlock**: `unlock()` cannot be called while already unlocked (`AlreadyUnlocked()` error). Hooks cannot re-enter the PoolManager via a second `unlock()`.

8. **Fee precision**: Fees are in hundredths of a bip (1e-6), NOT basis points. 3000 = 0.30%, not 30%.

9. **Dynamic fee flag**: A dynamic fee pool MUST set `PoolKey.fee` to exactly `0x800000`. Any other value with the high bit set is invalid.

10. **BalanceDelta packing**: Don't cast `BalanceDelta` directly to `int256` and interpret as a single number. Use `.amount0()` and `.amount1()` accessors.

## Checklist

### Pool Integration

- [ ] PoolKey currencies are sorted (`currency0 < currency1`)
- [ ] PoolKey fee is valid: either a static fee `<= 1_000_000` or exactly `DYNAMIC_FEE_FLAG`
- [ ] `tickSpacing > 0` and `<= type(int16).max`
- [ ] Pool is initialized before any swaps or liquidity operations
- [ ] Every balance-changing operation (`swap`, `modifyLiquidity`, `donate`, `take`, `settle`, `settleFor`, `clear`, `mint`, `burn`) runs inside an `unlockCallback`; only `initialize`, `sync`, `updateDynamicLPFee`, and the `IProtocolFees` admin functions are callable outside
- [ ] `unlockCallback` validates `msg.sender == address(poolManager)`

### Delta Resolution

- [ ] Every currency delta is resolved to zero before callback returns
- [ ] `sync(currency)` called before every ERC-20 transfer into PoolManager
- [ ] Native ETH settled via `settle{value: amount}()` (not sync + transfer)
- [ ] Positive deltas resolved via `take()`, not left dangling
- [ ] Consider `clear()` only for known-dust amounts (tokens are PERMANENTLY locked)

### Swap Integration

- [ ] `amountSpecified` sign is correct: negative = exact input, positive = exact output
- [ ] `sqrtPriceLimitX96` direction matches `zeroForOne` flag
- [ ] For unlimited slippage: `zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1`
- [ ] Hook return deltas are accounted for in final settlement
- [ ] Swap delta checked for expected bounds (slippage protection)

### Hook Development

- [ ] Hook address bits match `getHookPermissions()` return value
- [ ] All hook callbacks return the correct function selector
- [ ] `beforeSwap` fee override includes `LPFeeLibrary.OVERRIDE_FEE_FLAG`
- [ ] Dynamic fee values are `<= LPFeeLibrary.MAX_LP_FEE` (1_000_000)
- [ ] Hook state is keyed by `PoolId` (not raw PoolKey) for gas efficiency
- [ ] No reentrancy through `unlock()` from within hook callbacks
- [ ] Hook tested with exact-input AND exact-output swaps
- [ ] Gas overhead of hook callbacks measured and documented

### PositionManager Usage

- [ ] Permit2 allowances set for tokens before interacting with PositionManager
- [ ] Action sequences end with settlement actions (`SETTLE_PAIR`, `TAKE_PAIR`, etc.)
- [ ] `SWEEP` used to return excess tokens to the caller
- [ ] Deadline parameter set to prevent stale transactions
- [ ] Position tokenId tracked for future modifications
- [ ] Never use deprecated `MINT_POSITION_FROM_DELTAS` or `INCREASE_LIQUIDITY_FROM_DELTAS`

### Security

- [ ] Custom router validates `msg.sender == address(poolManager)` in `unlockCallback`
- [ ] Token approvals are scoped (no unlimited approvals to untrusted contracts)
- [ ] Slippage protection on all user-facing swap and liquidity functions
- [ ] Deployment addresses verified per-chain (NOT assumed to be cross-chain identical)
- [ ] `hookData` from untrusted callers is validated or bounded in size
- [ ] ERC-6909 claim token balances accounted for in protocol security model
