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.

Ir para a instalação

Informações da origem

Repositório
ccashwell/evm-cortex
Última atividade na origem
29 de setembro de 2026 às 14:14
Idioma detectado do SKILL.md
inglês
Estrelas
131
Forks
18

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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 | 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 | | `MIN_PRICE_LIMIT` | `uint160` | `TickMath.MIN_SQRT_PRICE + 1`, price limit for zeroForOne swaps | | `MAX_PRICE_LIMIT` | `uint160` | `TickMath.MAX_SQRT_PRICE - 1`, price limit for oneForZero swaps | | `LIQUIDITY_PARAMS` | `ModifyLiquidityParams` | Default add-liquidity params | | `REMOVE_LIQUIDITY_PARAMS` | `ModifyLiquidityParams` | Default remove-liquidity params | | `SWAP_PARAMS` | `SwapParams` | Default 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 | `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, 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"); }
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub