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.

Zur Installation springen

Quellinformationen

Repository
ccashwell/evm-cortex
Letzte Quellaktivität
10. April 2026 um 16:31
Erkannte Sprache von SKILL.md
Englisch
Sterne
131
Forks
18

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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,
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen