Skip to main content

uniswap-v4-testing

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.

Jump to install

Source facts

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

Install options

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

Review the source files

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

Showing SKILL.md

SKILL.md
Source instructions ยท Read-only preview
name
uniswap-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 {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; 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, IPoolManager.ModifyLiquidityParams({ tickLower: -120, tickUpper: 120, liquidityDelta: 10 ether, salt: bytes32(0) }), ZERO_BYTES ); } } ``` ### What `Deployers` Provides | Member | Type | Description | |--------|------|-------------| | `manager` | `IPoolManager` | Singleton PoolManager instance | | `swapRouter` | `PoolSwapTest` | Test router for swaps | | `modifyLiquidityRouter` | `PoolModifyLiquidityTest` | Test router for liquidity operations | | `donateRouter` | `PoolDonateTest` | Test router for donations | | `currency0`, `currency1` | `Currency` | Sorted test ERC-20 tokens (currency0 < currency1) | | `SQRT_PRICE_1_1` | `uint160` | sqrtPriceX96 for a 1:1 price ratio | | `SQRT_PRICE_1_2` | `uint160` | sqrtPriceX96 for a 1:2 price ratio | | `SQRT_PRICE_2_1` | `uint160` | sqrtPriceX96 for a 2:1 price ratio | | `SQRT_PRICE_1_4` | `uint160` | sqrtPriceX96 for a 1:4 price ratio | | `SQRT_PRICE_4_1` | `uint160` | sqrtPriceX96 for a 4:1 price ratio | | `ZERO_BYTES` | `bytes` | Empty bytes constant for hookData | | `MAX_TICK_SPACING` | `int24` | Maximum allowed tick spacing | ### 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 import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol"; 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, IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: 1 ether, // positive = 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, IPoolManager.SwapParams({ zeroForOne: false, amountSpecified: -0.5 ether, // negative = 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"); assertGt(delta.amount1(), 0, "should spend token1"); } ``` ### Swap Direction Reference | `zeroForOne` | `amountSpecified` | Meaning | |---|---|---| | `true` | `> 0` | Exact input of token0, receive token1 | | `true` | `< 0` | Receive exact output of token1, spend token0 | | `false` | `> 0` | Exact input of token1, receive token0 | | `false` | `< 0` | Receive 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, IPoolManager.SwapParams({ zeroForOne: zeroForOne, amountSpecified: int256(amountIn), 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, IPoolManager.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, IPoolManager.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, IPoolManager.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, IPoolManager.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, IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 }), hookData ); swapRouter.swap( key, IPoolManager.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, IPoolManager.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, IPoolManager.SwapParams({ zeroForOne: true,
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub