Agent skill

Uniswap V4 Expert

by ccashwell in ccashwell/evm-cortex

A skill your agent uses when building on, integrating with, or analyzing Uniswap V4.

MITAuto-check passedBusiness, Finance & HR

Install Uniswap V4 Expert

skills CLI
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install ccashwell/evm-cortex uniswap-v4-expert --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/uniswap-v4-expert .claude/skills/uniswap-v4-expert && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
uniswap-v4-expert
GitHub stars
131
Token cost
~9.1k tokens
SKILL.md length
2,155 words
Files
1
Skills in repo
89
Repo updated
First seen
Licence
MIT

At a glance

A skill your agent uses when building on, integrating with, or analyzing Uniswap V4.

  • Works in 7 steps: Caller invokes poolManager.unlock(data) → PoolManager calls… → Inside the callback, caller executes… → …
  • Integrating with
  • SKILL.md covers Architecture Overview, Core Types, PoolManager Interface and Parameter Structs, plus 2 more sections
  • Reaches mainnet.unichain.org

What it does

Uniswap V4 Expert is an agent skill from ccashwell/evm-cortex. 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.

Its SKILL.md is about 9.1k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Business, Finance & HR, covering Accounting and bookkeeping and Deployment. It works with Uniswap and Ethereum. The repository describes itself as: Ethereum protocol engineering squad for AI coding assistants. The licence is MIT.

When your agent uses it

  • Integrating with
  • Analyzing Uniswap V4

Example prompts

  • “/uniswap-v4-expert”

Workflow steps

7 steps, taken from the first numbered list in SKILL.md.

  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()

What it can do on your machine

Read from SKILL.md and the folder at commit f8f3301. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are solidity, bash and toml).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • mainnet.unichain.org

    Also links to:

    • developers.uniswap.org

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Uniswap V4 Expert loads about 9.1k tokens when it runs. Until then it costs about 88 tokens; SKILL.md has 2,155 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~88
When it runs · the whole SKILL.md, loaded when a task matches
~9.1k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from ccashwell/evm-cortex at commit f8f3301, republished under its MIT licence (© ccashwell). 2,155 words, ~9,105 tokens.

Download SKILL.mdSave it as .claude/skills/uniswap-v4-expert/SKILL.md (or your agent's skills folder).
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.feeEffective Fee
1000.01%
5000.05%
30000.30%
100001.00%
1000000100% (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)
ContractAddress
PoolManager0x000000000004444c5dc75cB358380D2e3dE08A90
Universal Router (V2)0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af
Universal Router 2.1.10x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA
Universal Router 2.1.20x23617e59A5925b2A4Bf75d73ff6711cD0b29De85
PositionManager0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e
PositionDescriptor0xd1428Ba554F4C8450b763a0B2040A4935c63f06C
StateView0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227
V4Quoter0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203
ReservesLens0x0000001b173C3bbF3984D417d8614E3eed34865B
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
Unichain (Chain ID: 130)
ContractAddress
PoolManager0x1F98400000000000000000000000000000000004
PositionManager0x4529A01c7A0410167c5740C487A8DE60232617bf
StateView0x86e8631A016F9068C3f085fAF484Ee3F5fDee8f2
V4Quoter0x333E3C607B141b18fF6de9f258db6e77fE7491E0
Universal Router (V2)0xEf740bf23aCaE26f6492B10de645D6B98dC8Eaf3
Universal Router 2.1.20xD1b797D92d87B688193A2B976eFc8D577D204343

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)
Show full SKILL.md (869 more words)Show less
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

AspectUniswap V3Uniswap V4
ArchitectureFactory + individual pool contractsSingleton PoolManager
Pool identificationContract addressPoolKey → PoolId (bytes32 hash)
Token transfersDirect transfers on every operationFlash accounting (deltas in transient storage)
Multi-hop efficiencyTransfer tokens between each poolNet settlement — only endpoints transfer
Native ETHMust wrap to WETH firstNative ETH via Currency.wrap(address(0))
ExtensibilityNo hook system10 hook callbacks (14 permission flags incl. 4 return-delta flags)
Fee modelFixed fee tiers (0.01%, 0.05%, 0.30%, 1%)Arbitrary static fees + dynamic fees via hooks
Fee distributionSwap fees onlydonate() for direct distribution to LPs
Position NFTsNonfungiblePositionManager (V3)PositionManager (still ERC-721) with Permit2 and batched Actions; ERC-6909 is for PoolManager claim tokens, not positions
LP fee updatesImmutable after pool creationDynamic via updateDynamicLPFee()
Transient storageNot used (pre-Cancun)EIP-1153 for delta tracking
Flash loansDedicated flash() functionImplicit via unlock — take first, settle later
Custom accountingNot possibleHooks can modify swap amounts via return deltas
Solidity version0.7.60.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

© ccashwell, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/uniswap-v4-expert of ccashwell/evm-cortex.

Open the folder on GitHubat commit f8f3301

Compare with similar skills

Uniswap V4 Expert next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Uniswap V4 Expert compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Uniswap V4 Expert this skillccashwell/evm-cortex131—~9.1kAutomated safety check: PassMIT
Notebook To Strategytradingstrategy-ai/trade-executor160—~1.1kAutomated safety check: PassCustom licence
Building Blocksaustintgriffith/ethskills295—~2.9kAutomated safety check: PassNone
Aomi Transactsickn33/agentic-awesome-skills47k1 repos~2.3kAutomated safety check: PassMIT
Addressesaustintgriffith/ethskills295—~7.1kAutomated safety check: PassNone
Juicebox V6BankrBot/skills1.2k—~1.5kAutomated safety check: PassMIT

Similar skills

  • Notebook To Strategy

    tradingstrategy-ai/trade-executor

    Transfer code from a backtesting Jupyter notebook to a Trade Executor strategy module

    160 GitHub stars~1.1k tokensUpdated 2 days ago
    Business, Finance & HRAuto-check passed
  • Building Blocks

    austintgriffith/ethskills

    DeFi legos and protocol composability on Ethereum and L2s. An agent skill from austintgriffith/ethskills.

    295 GitHub stars~2.9k tokensUpdated 1 mo ago
    Business, Finance & HRAuto-check passed
  • Aomi Transact

    sickn33/agentic-awesome-skills

    Build natural-language crypto/DeFi agents and EVM MCP plugins (Claude Code, Cursor, Codex, Gemini).

    47k GitHub starsUsed in 1 repo~2.3k tokens
    Business, Finance & HRAuto-check passed
  • Addresses

    austintgriffith/ethskills

    Verified contract addresses for major Ethereum protocols across mainnet and L2s.

    295 GitHub stars~7.1k tokensUpdated 1 mo ago
    Business, Finance & HRAuto-check passed
  • Juicebox V6

    BankrBot/skills

    Build, inspect, configure, and safely transact with Juicebox V6 projects, terminals, rulesets, hooks, tiered NFTs, Revnets, Croptop, Bendystraw, and omnichain deployments.

    1.2k GitHub stars~1.5k tokensUpdated yesterday
    Business, Finance & HRAuto-check passed
  • Xaut Trade

    LeoYeAI/openclaw-master-skills

    Buy or sell XAUT (Tether Gold) on Ethereum. An agent skill from LeoYeAI/openclaw-master-skills.

    2.2k GitHub stars~7.5k tokensUpdated 2 mo ago
    Business, Finance & HRAuto-check: notes

More from ccashwell/evm-cortex

All 89 skills in this repo
  • Xray Pre Audit

    ccashwell/evm-cortex

    A skill your agent uses when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview.

    131 GitHub stars~25k tokensUpdated 11 days ago
    Auto-check passed
  • Aave Integration

    ccashwell/evm-cortex

    A skill your agent uses when integrating with Aave V3 for lending, borrowing, flash loans, or building on top of Aave markets.

    131 GitHub stars~1.3k tokensUpdated 11 days ago
    Auto-check passed
  • Access Control Patterns

    ccashwell/evm-cortex

    Access control design patterns for Solidity protocols. An agent skill from ccashwell/evm-cortex.

    131 GitHub stars~1.8k tokensUpdated 11 days ago
    Auto-check passed
  • Anvil Patterns

    ccashwell/evm-cortex

    A skill your agent uses when running a local Ethereum node with Anvil.

    131 GitHub stars~1.3k tokensUpdated 11 days ago
    Auto-check passed
  • Audit Breadth Scan

    ccashwell/evm-cortex

    A skill your agent uses when performing systematic breadth-first review of all contracts during a security audit.

    131 GitHub stars~1.4k tokensUpdated 11 days ago
    Auto-check passed
  • Audit Depth Analysis

    ccashwell/evm-cortex

    A skill your agent uses when performing deep analysis of specific findings or high-risk areas during a security audit.

    131 GitHub stars~1.6k tokensUpdated 11 days ago
    Auto-check passed

Works with

Questions about Uniswap V4 Expert

What does Uniswap V4 Expert do?

A skill your agent uses when building on, integrating with, or analyzing Uniswap V4. Uniswap V4 Expert is an agent skill from ccashwell/evm-cortex. Use when building on, integrating with, or analyzing Uniswap V4.

When should I use Uniswap V4 Expert?

Uniswap V4 Expert fits situations like: integrating with; analyzing Uniswap V4.

How do I install Uniswap V4 Expert in Claude Code?

Run `npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a claude-code`. Or copy the skill folder (skills/uniswap-v4-expert in ccashwell/evm-cortex) into .claude/skills/uniswap-v4-expert in your project. Claude Code loads it when a task matches its description.

How do I install Uniswap V4 Expert in Codex?

Run `npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a codex`. Or copy the skill folder (skills/uniswap-v4-expert in ccashwell/evm-cortex) into .agents/skills/uniswap-v4-expert in your project. Codex loads it when a task matches its description.

Can I use Uniswap V4 Expert in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/uniswap-v4-expert, .gemini/skills/uniswap-v4-expert, .github/skills/uniswap-v4-expert and .opencode/skills/uniswap-v4-expert in your project.

What does Uniswap V4 Expert need to run?

SKILL.md names no scripts, command-line tools or credentials: Uniswap V4 Expert is instructions for the agent only.

Does Uniswap V4 Expert access the network?

SKILL.md names 2 domains. In commands or code: mainnet.unichain.org; the agent is likely to contact it when it follows the instructions. As links in the text: developers.uniswap.org. This is read from the text; nothing was executed.

Is Uniswap V4 Expert safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Uniswap V4 Expert use?

Uniswap V4 Expert is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Uniswap V4 Expert use?

About 9.1k tokens (SKILL.md is roughly 36k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Uniswap V4 Expert?

Skills that share tags, products or a category with Uniswap V4 Expert: Notebook To Strategy (tradingstrategy-ai/trade-executor, 160 stars), Building Blocks (austintgriffith/ethskills, 295 stars), Aomi Transact (sickn33/agentic-awesome-skills, 47k stars) and Addresses (austintgriffith/ethskills, 295 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Uniswap V4 Expert?

ccashwell (a GitHub user) maintains it in ccashwell/evm-cortex, which has 131 GitHub stars. The repository holds 89 skills in this directory. The repository was last updated on September 30, 2026.

Source: ccashwell/evm-cortex on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.