Agent skill

Uniswap V4 Testing

by ccashwell in ccashwell/evm-cortex

A skill your agent uses when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions.

MITAuto-check passedBusiness, Finance & HR

Install Uniswap V4 Testing

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

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

GitHub CLI
$ gh skill install ccashwell/evm-cortex uniswap-v4-testing --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-testing .claude/skills/uniswap-v4-testing && 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-testing
GitHub stars
131
Token cost
~8.2k tokens
SKILL.md length
582 words
Files
1
Skills in repo
89
Repo updated
First seen
Licence
MIT

At a glance

A skill your agent uses when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions.

  • Writing Foundry tests for Uniswap V4 hooks
  • SKILL.md covers Test Setup with Deployers, HookMiner for Address Mining, Swap Test Patterns and Liquidity Test Patterns, plus 6 more sections
  • Reaches mainnet.unichain.org
  • Router integrations

What it does

Uniswap V4 Testing is an agent skill from ccashwell/evm-cortex. Use when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions. Covers test setup with Deployers, HookMiner for address mining, swap/liquidity test patterns, gas profiling, fork testing against production pools, and invariant testing for custom hooks.

Its SKILL.md is about 8.2k 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. It works with Uniswap. The repository describes itself as: Ethereum protocol engineering squad for AI coding assistants. The licence is MIT.

When your agent uses it

  • Writing Foundry tests for Uniswap V4 hooks
  • Router integrations
  • Pool interactions

Example prompts

  • “/uniswap-v4-testing”

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

    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 Testing loads about 8.2k tokens when it runs. Until then it costs about 76 tokens; SKILL.md has 582 words of instructions outside code blocks.

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

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). 582 words, ~8,160 tokens.

Download SKILL.mdSave it as .claude/skills/uniswap-v4-testing/SKILL.md (or your agent's skills folder).
name
uniswap-v4-testing
description
Use when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions. Covers test setup with Deployers, HookMiner for address mining, swap/liquidity test patterns, gas profiling, fork testing against production pools, and invariant testing for custom hooks.

Uniswap V4 Testing with Foundry

Test Setup with Deployers

The Deployers base contract from v4-core bootstraps a complete V4 environment — PoolManager, test routers, currencies, and standard constants.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import {Deployers} from "v4-core/test/utils/Deployers.sol";
import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
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} from "v4-core/src/types/BalanceDelta.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";

import {MyHook} from "../src/MyHook.sol";

contract MyHookTest is Test, Deployers {
    using PoolIdLibrary for PoolKey;
    using StateLibrary for IPoolManager;
    using CurrencyLibrary for Currency;

    MyHook hook;
    // `PoolKey key` is inherited from Deployers; redeclaring it is a DeclarationError
    PoolId poolId;

    function setUp() public {
        deployFreshManagerAndRouters();
        deployMintAndApprove2Currencies();

        // Deploy hook to mined address (see HookMiner section)
        _deployHook();

        key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(hook)));
        manager.initialize(key, SQRT_PRICE_1_1);
        poolId = key.toId();

        modifyLiquidityRouter.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: -120,
                tickUpper: 120,
                liquidityDelta: 10 ether,
                salt: bytes32(0)
            }),
            ZERO_BYTES
        );
    }
}
What Deployers Provides
MemberTypeDescription
managerIPoolManagerSingleton PoolManager instance
swapRouterPoolSwapTestTest router for swaps
modifyLiquidityRouterPoolModifyLiquidityTestTest router for liquidity operations
donateRouterPoolDonateTestTest router for donations
currency0, currency1CurrencySorted test ERC-20 tokens (currency0 < currency1)
SQRT_PRICE_1_1uint160sqrtPriceX96 for a 1:1 price ratio
SQRT_PRICE_1_2uint160sqrtPriceX96 for a 1:2 price ratio
SQRT_PRICE_2_1uint160sqrtPriceX96 for a 2:1 price ratio
SQRT_PRICE_1_4uint160sqrtPriceX96 for a 1:4 price ratio
SQRT_PRICE_4_1uint160sqrtPriceX96 for a 4:1 price ratio
ZERO_BYTESbytesEmpty bytes constant for hookData
MIN_PRICE_LIMITuint160TickMath.MIN_SQRT_PRICE + 1, price limit for zeroForOne swaps
MAX_PRICE_LIMITuint160TickMath.MAX_SQRT_PRICE - 1, price limit for oneForZero swaps
LIQUIDITY_PARAMSModifyLiquidityParamsDefault add-liquidity params
REMOVE_LIQUIDITY_PARAMSModifyLiquidityParamsDefault remove-liquidity params
SWAP_PARAMSSwapParamsDefault exact-input swap (amountSpecified: -100, zeroForOne)
Key Deployer Functions
solidity
// Deploy PoolManager + all test routers
deployFreshManagerAndRouters();

// Deploy two sorted ERC-20 tokens, mint to address(this), approve all routers
deployMintAndApprove2Currencies();

// Deploy PoolManager only
deployFreshManager();

HookMiner for Address Mining

Hook addresses encode permissions in their leading bits. HookMiner brute-forces a CREATE2 salt that produces an address with the correct bit pattern.

solidity
// HookMiner left v4-periphery/src on 2026-02-06 (PR #510); it now lives under test/shared
import {HookMiner} from "v4-periphery/test/shared/HookMiner.sol";
// or: import {HookMiner} from "v4-hooks-public/src/utils/HookMiner.sol"; (byte-identical)

function _deployHook() internal {
    uint160 flags = uint160(
        Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG
    );

    bytes memory constructorArgs = abi.encode(manager);
    (address hookAddress, bytes32 salt) = HookMiner.find(
        address(this),
        flags,
        type(MyHook).creationCode,
        constructorArgs
    );

    hook = new MyHook{salt: salt}(manager);
    require(address(hook) == hookAddress, "hook address mismatch");
}
Common Flag Combinations
solidity
// Swap-only hook
uint160 swapFlags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG);

// Liquidity management hook
uint160 liqFlags = uint160(
    Hooks.BEFORE_ADD_LIQUIDITY_FLAG
    | Hooks.AFTER_ADD_LIQUIDITY_FLAG
    | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
    | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG
);

// Full lifecycle hook
uint160 fullFlags = uint160(
    Hooks.BEFORE_INITIALIZE_FLAG
    | Hooks.AFTER_INITIALIZE_FLAG
    | Hooks.BEFORE_SWAP_FLAG
    | Hooks.AFTER_SWAP_FLAG
    | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
    | Hooks.AFTER_ADD_LIQUIDITY_FLAG
    | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG
    | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG
);

// Hook that modifies swap deltas
uint160 deltaFlags = uint160(
    Hooks.BEFORE_SWAP_FLAG
    | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG
);

Swap Test Patterns

Exact Input — zeroForOne
solidity
function test_swapExactInput_zeroForOne() public {
    uint256 balance0Before = currency0.balanceOf(address(this));
    uint256 balance1Before = currency1.balanceOf(address(this));

    BalanceDelta delta = swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,  // negative = exact input
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({
            takeClaims: false,
            settleUsingBurn: false
        }),
        ZERO_BYTES
    );

    assertLt(delta.amount0(), 0, "should spend token0");
    assertGt(delta.amount1(), 0, "should receive token1");
    assertLt(currency0.balanceOf(address(this)), balance0Before, "token0 balance decreased");
    assertGt(currency1.balanceOf(address(this)), balance1Before, "token1 balance increased");
}
Exact Output — oneForZero
solidity
function test_swapExactOutput_oneForZero() public {
    BalanceDelta delta = swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: false,
            amountSpecified: 0.5 ether,  // positive = exact output
            sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    assertEq(delta.amount0(), 0.5 ether, "should receive exactly 0.5 token0");
    assertLt(delta.amount1(), 0, "should spend token1");
}
Swap Direction Reference
zeroForOneamountSpecifiedMeaning
true< 0Exact input of token0, receive token1
true> 0Receive exact output of token1, spend token0
false< 0Exact input of token1, receive token0
false> 0Receive exact output of token0, spend token1
Price Limits

Always set price limits to avoid reverts:

solidity
// zeroForOne = true → price goes DOWN → use MIN as limit
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1

// zeroForOne = false → price goes UP → use MAX as limit
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
Fuzz Testing Swaps
solidity
function testFuzz_swap(uint256 amountIn, bool zeroForOne) public {
    amountIn = bound(amountIn, 1e15, 5 ether);

    uint160 priceLimit = zeroForOne
        ? TickMath.MIN_SQRT_PRICE + 1
        : TickMath.MAX_SQRT_PRICE - 1;

    BalanceDelta delta = swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: zeroForOne,
            amountSpecified: -int256(amountIn),  // negative = exact input
            sqrtPriceLimitX96: priceLimit
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    if (zeroForOne) {
        assertLt(delta.amount0(), 0);
        assertGt(delta.amount1(), 0);
    } else {
        assertGt(delta.amount0(), 0);
        assertLt(delta.amount1(), 0);
    }
}

Liquidity Test Patterns

Adding Liquidity
solidity
function test_addLiquidity() public {
    uint256 balance0Before = currency0.balanceOf(address(this));
    uint256 balance1Before = currency1.balanceOf(address(this));

    BalanceDelta delta = modifyLiquidityRouter.modifyLiquidity(
        key,
        ModifyLiquidityParams({
            tickLower: -600,
            tickUpper: 600,
            liquidityDelta: 5 ether,
            salt: bytes32(0)
        }),
        ZERO_BYTES
    );

    assertLt(delta.amount0(), 0, "should deposit token0");
    assertLt(delta.amount1(), 0, "should deposit token1");
    assertLt(currency0.balanceOf(address(this)), balance0Before);
    assertLt(currency1.balanceOf(address(this)), balance1Before);
}
Removing Liquidity
solidity
function test_removeLiquidity() public {
    uint256 balance0Before = currency0.balanceOf(address(this));
    uint256 balance1Before = currency1.balanceOf(address(this));

    BalanceDelta delta = modifyLiquidityRouter.modifyLiquidity(
        key,
        ModifyLiquidityParams({
            tickLower: -120,
            tickUpper: 120,
            liquidityDelta: -5 ether,  // negative = remove
            salt: bytes32(0)
        }),
        ZERO_BYTES
    );

    assertGt(delta.amount0(), 0, "should withdraw token0");
    assertGt(delta.amount1(), 0, "should withdraw token1");
    assertGt(currency0.balanceOf(address(this)), balance0Before);
    assertGt(currency1.balanceOf(address(this)), balance1Before);
}
Out-of-Range Liquidity
solidity
function test_addLiquidity_outOfRange() public {
    (uint160 sqrtPriceX96, int24 currentTick,,) = manager.getSlot0(poolId);

    // Add liquidity entirely above current price (only token1 deposited)
    int24 tickLower = currentTick + 120;
    int24 tickUpper = currentTick + 600;
    // Round to tick spacing
    tickLower = (tickLower / key.tickSpacing) * key.tickSpacing;
    tickUpper = (tickUpper / key.tickSpacing) * key.tickSpacing;

    BalanceDelta delta = modifyLiquidityRouter.modifyLiquidity(
        key,
        ModifyLiquidityParams({
            tickLower: tickLower,
            tickUpper: tickUpper,
            liquidityDelta: 1 ether,
            salt: bytes32(0)
        }),
        ZERO_BYTES
    );

    assertEq(delta.amount0(), 0, "no token0 for above-range position");
    assertLt(delta.amount1(), 0, "should deposit token1 only");
}

Hook Callback Testing

Verifying Hook Side Effects
solidity
function test_hookCalledOnSwap() public {
    uint256 swapCountBefore = hook.swapCount(poolId);

    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    assertEq(hook.swapCount(poolId), swapCountBefore + 1, "hook should increment counter");
}
Verifying Hook Receives Correct Parameters
solidity
function test_hookReceivesCorrectParams() public {
    bytes memory hookData = abi.encode(uint256(42));

    vm.expectEmit(address(hook));
    emit MyHook.BeforeSwapCalled(
        address(swapRouter),
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        hookData
    );

    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        hookData
    );
}
Testing Hooks That Return Deltas
solidity
function test_hookReturnsDelta() public {
    // For hooks with BEFORE_SWAP_RETURNS_DELTA_FLAG, the hook can take/give tokens
    uint256 hookBalance0Before = currency0.balanceOf(address(hook));

    BalanceDelta delta = swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    // Verify the hook captured its fee or modified the delta
    uint256 hookBalance0After = currency0.balanceOf(address(hook));
    assertGt(hookBalance0After, hookBalance0Before, "hook should have taken fee");
}
Testing afterInitialize
solidity
function test_afterInitialize_setsState() public {
    // Deploy a second pool to test initialization
    PoolKey memory key2 = PoolKey(
        currency0, currency1, 500, 10, IHooks(address(hook))
    );
    manager.initialize(key2, SQRT_PRICE_1_1);
    PoolId id2 = key2.toId();

    assertEq(hook.poolInitTimestamp(id2), block.timestamp);
}

Reading Pool State in Tests

solidity
using StateLibrary for IPoolManager;

function test_poolStateAfterSwap() public {
    (uint160 sqrtPriceBefore, int24 tickBefore,,) = manager.getSlot0(poolId);

    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    (uint160 sqrtPriceAfter, int24 tickAfter, uint24 protocolFee, uint24 lpFee) =
        manager.getSlot0(poolId);

    assertLt(sqrtPriceAfter, sqrtPriceBefore, "price should decrease on zeroForOne swap");
    assertLe(tickAfter, tickBefore, "tick should decrease or stay same");
}

function test_poolLiquidity() public {
    uint128 totalLiquidity = manager.getLiquidity(poolId);
    assertGt(totalLiquidity, 0, "pool should have liquidity from setUp");

    // StateLibrary.getPositionLiquidity takes (poolId, bytes32 positionId); the
    // owner/tick/salt form is getPositionInfo, which also returns fee growth
    (uint128 positionLiquidity,,) = manager.getPositionInfo(
        poolId,
        address(modifyLiquidityRouter),
        -120,
        120,
        bytes32(0)
    );
    assertGt(positionLiquidity, 0, "position should have liquidity");
}

Dynamic Fee Hook Testing

solidity
import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";

function test_dynamicFeeHook() public {
    PoolKey memory dynamicKey = PoolKey(
        currency0,
        currency1,
        LPFeeLibrary.DYNAMIC_FEE_FLAG,
        60,
        IHooks(address(hook))
    );
    manager.initialize(dynamicKey, SQRT_PRICE_1_1);

    modifyLiquidityRouter.modifyLiquidity(
        dynamicKey,
        ModifyLiquidityParams({
            tickLower: -120,
            tickUpper: 120,
            liquidityDelta: 10 ether,
            salt: bytes32(0)
        }),
        ZERO_BYTES
    );

    // Swap under normal conditions → expect base fee
    BalanceDelta delta1 = swapRouter.swap(
        dynamicKey,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -0.1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    // Change conditions that affect fee (e.g., volatility, time)
    vm.warp(block.timestamp + 1 hours);

    BalanceDelta delta2 = swapRouter.swap(
        dynamicKey,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -0.1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );

    // Under higher volatility the fee should differ
    // (exact assertion depends on hook logic)
}

Fork Testing Against Production Pools

solidity
contract V4ForkTest is Test {
    using StateLibrary for IPoolManager;

    // EIP-55 checksum matters: Solidity rejects a mixed-case literal with a wrong checksum
    IPoolManager constant PM = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90);

    function setUp() public {
        // The PoolManager first has code at block 21_688_329 (2025-01-23); fork after that
        vm.createSelectFork(vm.envString("ETH_RPC_URL"), 23_000_000);
    }

    function test_productionPoolState() public view {
        // Canonical live pool: native ETH / USDC, 0.05% fee, tickSpacing 10, no hook
        // PoolId 0x21c67e77068de97969ba93d4aab21826d33ca12bb9f565d8496e8fda8a82ca27
        // (initialized at block 21_688_545 on 2025-01-23, so any later fork block works)
        PoolKey memory liveKey = PoolKey({
            currency0: Currency.wrap(address(0)),                                  // native ETH
            currency1: Currency.wrap(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48), // USDC
            fee: 500,
            tickSpacing: int24(10),
            hooks: IHooks(address(0))
        });

        PoolId liveId = PoolIdLibrary.toId(liveKey);
        (uint160 sqrtPriceX96, int24 tick,,) = PM.getSlot0(liveId);
        assertGt(sqrtPriceX96, 0, "pool should exist");
    }

    function test_deployHookAgainstProductionPM() public {
        // Deploy your hook to work with the production PoolManager
        // Useful for integration testing with real pool state
    }
}
L2 Fork (Unichain)

PoolManager addresses differ per chain. On Unichain (chain ID 130, RPC https://mainnet.unichain.org) the PoolManager is 0x1F98400000000000000000000000000000000004 (verified with cast code); the Ethereum address has no code there.

solidity
IPoolManager constant UNICHAIN_PM = IPoolManager(0x1F98400000000000000000000000000000000004);

function setUp() public {
    vm.createSelectFork(vm.envString("UNICHAIN_RPC_URL"));
}

Gas Profiling

CLI Commands
bash
# Gas report for all hook tests
forge test --gas-report --match-contract MyHookTest

# Snapshot current gas usage
forge snapshot --match-contract MyHookTest

# Compare against baseline
forge snapshot --diff .gas-snapshot

# Fail if gas increased beyond threshold
forge snapshot --check --tolerance 5
In-Test Gas Measurement
solidity
function test_hookGasOverhead() public {
    uint256 gasBefore = gasleft();
    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
    uint256 gasUsed = gasBefore - gasleft();

    emit log_named_uint("swap gas with hook", gasUsed);
    assertLt(gasUsed, 200_000, "total swap gas too high");
}

function test_compareGasWithAndWithoutHook() public {
    // Pool with hook
    uint256 gasBefore = gasleft();
    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -0.1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
    uint256 gasWithHook = gasBefore - gasleft();

    // Pool without hook (no-op hook address)
    PoolKey memory bareKey = PoolKey(
        currency0, currency1, 3000, 60, IHooks(address(0))
    );
    manager.initialize(bareKey, SQRT_PRICE_1_1);
    modifyLiquidityRouter.modifyLiquidity(
        bareKey,
        ModifyLiquidityParams({
            tickLower: -120, tickUpper: 120,
            liquidityDelta: 10 ether, salt: bytes32(0)
        }),
        ZERO_BYTES
    );

    gasBefore = gasleft();
    swapRouter.swap(
        bareKey,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -0.1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
    uint256 gasWithoutHook = gasBefore - gasleft();

    uint256 overhead = gasWithHook - gasWithoutHook;
    emit log_named_uint("hook overhead (gas)", overhead);
    // There is no universal hook gas threshold: record the overhead with
    // `forge snapshot` and review regressions with `forge snapshot --check`
}

Invariant Testing for Hooks

Handler Contract
solidity
contract HookHandler is Test, Deployers {
    using PoolIdLibrary for PoolKey;

    MyHook public hook;
    // `key` is inherited from Deployers

    constructor(MyHook _hook, PoolKey memory _key) {
        hook = _hook;
        key = _key;
    }

    function swap(uint256 amountSeed, bool zeroForOne) external {
        uint256 amount = bound(amountSeed, 1e15, 2 ether);

        uint160 priceLimit = zeroForOne
            ? TickMath.MIN_SQRT_PRICE + 1
            : TickMath.MAX_SQRT_PRICE - 1;

        try swapRouter.swap(
            key,
            SwapParams({
                zeroForOne: zeroForOne,
                amountSpecified: -int256(amount),  // negative = exact input
                sqrtPriceLimitX96: priceLimit
            }),
            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
            ZERO_BYTES
        ) {} catch {}
    }

    function addLiquidity(uint256 liquiditySeed) external {
        uint256 liquidity = bound(liquiditySeed, 1e16, 5 ether);
        try modifyLiquidityRouter.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: -600,
                tickUpper: 600,
                liquidityDelta: int256(liquidity),
                salt: bytes32(0)
            }),
            ZERO_BYTES
        ) {} catch {}
    }
}
Invariant Test Contract
solidity
contract MyHookInvariantTest is Test, Deployers {
    using StateLibrary for IPoolManager;
    using PoolIdLibrary for PoolKey;

    MyHook hook;
    PoolId poolId;
    HookHandler handler;

    function setUp() public {
        deployFreshManagerAndRouters();
        deployMintAndApprove2Currencies();
        _deployHook();

        key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(hook)));
        manager.initialize(key, SQRT_PRICE_1_1);
        poolId = key.toId();

        modifyLiquidityRouter.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: -600, tickUpper: 600,
                liquidityDelta: 100 ether, salt: bytes32(0)
            }),
            ZERO_BYTES
        );

        handler = new HookHandler(hook, key);

        // Fund handler
        currency0.transfer(address(handler), 1000 ether);
        currency1.transfer(address(handler), 1000 ether);

        targetContract(address(handler));
    }

    function invariant_poolManagerSolvent() public view {
        uint256 pm0 = currency0.balanceOf(address(manager));
        uint256 pm1 = currency1.balanceOf(address(manager));
        assertGe(pm0, 0, "PoolManager should never have negative token0");
        assertGe(pm1, 0, "PoolManager should never have negative token1");
    }

    function invariant_hookStateConsistent() public view {
        // Hook per-pool state should remain internally consistent
        uint256 totalSwaps = hook.swapCount(poolId);
        assertGe(totalSwaps, 0, "swap count should be non-negative");
    }

    function invariant_priceWithinBounds() public view {
        (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId);
        assertGe(sqrtPriceX96, TickMath.MIN_SQRT_PRICE);
        assertLe(sqrtPriceX96, TickMath.MAX_SQRT_PRICE);
    }
}
Foundry Invariant Config

In foundry.toml:

toml
[invariant]
runs = 256
depth = 64
fail_on_revert = false

Testing HookData Pass-Through

Hooks receive arbitrary bytes calldata hookData — test that your hook correctly parses and acts on it.

solidity
function test_hookDataPassedToBeforeSwap() public {
    bytes memory hookData = abi.encode(address(this), uint256(100));

    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        hookData
    );

    // Assert hook decoded and used the data
    assertEq(hook.lastCaller(poolId), address(this));
    assertEq(hook.lastParam(poolId), 100);
}

function test_emptyHookData() public {
    // Hook should handle empty hookData gracefully
    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
}
Show full SKILL.md (228 more words)Show less

Testing Revert Conditions

solidity
function test_revert_uninitializedPool() public {
    PoolKey memory badKey = PoolKey(
        currency0, currency1, 500, 10, IHooks(address(hook))
    );
    // Pool not initialized — swap should revert
    vm.expectRevert();
    swapRouter.swap(
        badKey,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
}

function test_revert_swapZeroAmount() public {
    vm.expectRevert();
    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: 0,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
}

function test_revert_hookCustomError() public {
    // If the hook enforces conditions via custom errors
    vm.expectRevert(abi.encodeWithSelector(MyHook.SwapPaused.selector, poolId));
    hook.setPaused(poolId, true);
    swapRouter.swap(
        key,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
}

Native ETH (Currency.wrap(address(0))) Pools

solidity
function test_nativeETHPool() public {
    PoolKey memory ethKey = PoolKey(
        CurrencyLibrary.ADDRESS_ZERO,  // native ETH as currency0
        currency1,
        3000,
        60,
        IHooks(address(0))
    );
    manager.initialize(ethKey, SQRT_PRICE_1_1);

    modifyLiquidityRouter.modifyLiquidity{value: 10 ether}(
        ethKey,
        ModifyLiquidityParams({
            tickLower: -120,
            tickUpper: 120,
            liquidityDelta: 10 ether,
            salt: bytes32(0)
        }),
        ZERO_BYTES
    );

    swapRouter.swap{value: 1 ether}(
        ethKey,
        SwapParams({
            zeroForOne: true,
            amountSpecified: -1 ether,
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
        }),
        PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
        ZERO_BYTES
    );
}

Test File Organization

test/
├── unit/
│   ├── MyHook.t.sol              # Core hook logic tests
│   └── MyHookPermissions.t.sol   # Permission and access tests
├── integration/
│   ├── SwapWithHook.t.sol        # Swap integration tests
│   └── LiquidityWithHook.t.sol   # Liquidity integration tests
├── invariant/
│   ├── handlers/HookHandler.sol  # Invariant handler
│   └── MyHook.invariant.t.sol    # Invariant properties
├── fork/
│   └── V4Mainnet.t.sol           # Fork tests against production
└── helpers/
    └── HookTestBase.sol          # Shared setUp and utilities
Shared Base Contract
solidity
abstract contract HookTestBase is Test, Deployers {
    using PoolIdLibrary for PoolKey;
    using StateLibrary for IPoolManager;

    MyHook hook;
    PoolId poolId;

    function setUp() public virtual {
        deployFreshManagerAndRouters();
        deployMintAndApprove2Currencies();
        _deployHook();

        key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(hook)));
        manager.initialize(key, SQRT_PRICE_1_1);
        poolId = key.toId();
    }

    function _deployHook() internal {
        uint160 flags = uint160(
            Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG
        );
        bytes memory constructorArgs = abi.encode(manager);
        (address hookAddress, bytes32 salt) = HookMiner.find(
            address(this), flags, type(MyHook).creationCode, constructorArgs
        );
        hook = new MyHook{salt: salt}(manager);
        require(address(hook) == hookAddress, "hook address mismatch");
    }

    function _seedLiquidity() internal {
        modifyLiquidityRouter.modifyLiquidity(
            key,
            ModifyLiquidityParams({
                tickLower: -600, tickUpper: 600,
                liquidityDelta: 100 ether, salt: bytes32(0)
            }),
            ZERO_BYTES
        );
    }

    function _swapExactIn(bool zeroForOne, uint256 amount) internal returns (BalanceDelta) {
        return swapRouter.swap(
            key,
            SwapParams({
                zeroForOne: zeroForOne,
                amountSpecified: -int256(amount),  // negative = exact input
                sqrtPriceLimitX96: zeroForOne
                    ? TickMath.MIN_SQRT_PRICE + 1
                    : TickMath.MAX_SQRT_PRICE - 1
            }),
            PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
            ZERO_BYTES
        );
    }
}

Hook Testing Checklist

  • Setup: deployFreshManagerAndRouters() and deployMintAndApprove2Currencies() called
  • Address mining: Hook deployed via HookMiner with correct permission flags
  • Permissions: getHookPermissions() matches the mined address bits
  • Return selectors: Every callback returns its own BaseHook.<function>.selector
  • Swap: exact input zeroForOne: token0 spent, token1 received
  • Swap: exact input oneForZero: token1 spent, token0 received
  • Swap: exact output zeroForOne: exact token1 received
  • Swap: exact output oneForZero: exact token0 received
  • Liquidity: add in-range: both tokens deposited
  • Liquidity: add out-of-range: only one token deposited
  • Liquidity: remove: tokens returned to LP
  • Hook state: per-pool state updated correctly on callbacks
  • hookData: arbitrary bytes decoded and used correctly
  • Empty hookData: hook handles ZERO_BYTES without reverting
  • Dynamic fees: fee varies under different conditions when DYNAMIC_FEE_FLAG set
  • Delta returns: hooks with RETURNS_DELTA flags modify swap/liquidity amounts correctly
  • Revert paths: invalid inputs, paused states, and unauthorized access revert correctly
  • Native ETH: pool with Currency.wrap(address(0)) works with hook
  • Gas overhead: hook callback overhead measured and documented via forge snapshot (no universal threshold; review regressions with --check)
  • Gas comparison: overhead vs no-hook pool documented
  • Invariant: PM solvency: PoolManager balances never go negative
  • Invariant: price bounds: sqrtPriceX96 stays within [MIN, MAX]
  • Invariant: hook state: per-pool counters and accumulators remain consistent
  • Fork test: hook tested against production PoolManager on mainnet fork
  • Fuzz tests: swaps and liquidity ops fuzzed with bound() for amount ranges

© 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-testing of ccashwell/evm-cortex.

Open the folder on GitHubat commit f8f3301

Compare with similar skills

Uniswap V4 Testing 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 Testing compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Uniswap V4 Testing this skillccashwell/evm-cortex131—~8.2kAutomated safety check: PassMIT
Notebook To Strategytradingstrategy-ai/trade-executor160—~1.1kAutomated safety check: PassCustom licence
Building Blocksaustintgriffith/ethskills295—~2.9kAutomated safety check: PassNone
Oracle Flashloan Analysisquillai-network/quillshield_skills130—~2.8kAutomated safety check: PassMIT
Octav APIinternet-court/internet-court-skill6.5k—~3.8kAutomated safety check: PassMIT
Uniswap SwapNethereum/Nethereum2.3k—~2.1kAutomated 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 yesterday
    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
  • Oracle Flashloan Analysis

    quillai-network/quillshield_skills

    Detects price oracle manipulation and flash loan attack vectors in DeFi smart contracts.

    130 GitHub stars~2.8k tokensUpdated 6 mo ago
    Business, Finance & HRAuto-check passed
  • Octav API

    internet-court/internet-court-skill

    Integrate with Octav API for cryptocurrency portfolio tracking, transaction history, and DeFi analytics across 50+ blockchain networks.

    6.5k GitHub stars~3.8k tokensUpdated 1 mo ago
    Business, Finance & HRAuto-check passed
  • Uniswap Swap

    Nethereum/Nethereum

    Swap tokens on Uniswap V2/V3/V4 using Nethereum (.NET/C). An agent skill from Nethereum/Nethereum.

    2.3k GitHub stars~2.1k tokensUpdated 4 days 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

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 9 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 9 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 9 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 9 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 9 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 9 days ago
    Auto-check passed

Works with

Questions about Uniswap V4 Testing

What does Uniswap V4 Testing do?

A skill your agent uses when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions. Uniswap V4 Testing is an agent skill from ccashwell/evm-cortex. Use when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions.

When should I use Uniswap V4 Testing?

Uniswap V4 Testing fits situations like: writing Foundry tests for Uniswap V4 hooks; router integrations; pool interactions.

How do I install Uniswap V4 Testing in Claude Code?

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

How do I install Uniswap V4 Testing in Codex?

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

Can I use Uniswap V4 Testing 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-testing -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-testing, .gemini/skills/uniswap-v4-testing, .github/skills/uniswap-v4-testing and .opencode/skills/uniswap-v4-testing in your project.

What does Uniswap V4 Testing need to run?

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

Does Uniswap V4 Testing access the network?

SKILL.md names 1 domain. In commands or code: mainnet.unichain.org; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Uniswap V4 Testing 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 Testing use?

Uniswap V4 Testing 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 Testing use?

About 8.2k tokens (SKILL.md is roughly 33k 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 Testing?

Skills that share tags, products or a category with Uniswap V4 Testing: Notebook To Strategy (tradingstrategy-ai/trade-executor, 160 stars), Building Blocks (austintgriffith/ethskills, 295 stars), Oracle Flashloan Analysis (quillai-network/quillshield_skills, 130 stars) and Octav API (internet-court/internet-court-skill, 6.5k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Uniswap V4 Testing?

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.