Notebook To Strategy
tradingstrategy-ai/trade-executor
Transfer code from a backtesting Jupyter notebook to a Trade Executor strategy module
A skill your agent uses when writing Foundry tests for Uniswap V4 hooks, router integrations, or pool interactions.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-testing -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-testing --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "uniswap-v4-testing" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testing into .claude/skills/uniswap-v4-testing/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-testing", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testingType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-testing -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-testing --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/uniswap-v4-testing .agents/skills/uniswap-v4-testing && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "uniswap-v4-testing" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testing into .agents/skills/uniswap-v4-testing/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-testing", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-testing -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-testing --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/uniswap-v4-testing .cursor/skills/uniswap-v4-testing && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "uniswap-v4-testing" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testing into .cursor/skills/uniswap-v4-testing/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-testing", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ccashwell/evm-cortex.git --path skills/uniswap-v4-testing--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-testing -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-testing --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/uniswap-v4-testing .gemini/skills/uniswap-v4-testing && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "uniswap-v4-testing" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testing into .gemini/skills/uniswap-v4-testing/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-testing", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ccashwell/evm-cortex uniswap-v4-testingInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-testing -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/uniswap-v4-testing .github/skills/uniswap-v4-testing && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "uniswap-v4-testing" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testing into .github/skills/uniswap-v4-testing/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-testing", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-testing -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-testing --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/uniswap-v4-testing .opencode/skills/uniswap-v4-testing && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "uniswap-v4-testing" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-testing into .opencode/skills/uniswap-v4-testing/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-testing", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
uniswap-v4-testingA 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. 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.
Read from SKILL.md and the folder at commit f8f3301. It shows what the files ask for, not the result of running them.
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.
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.
Hosts in commands or code, which the agent is likely to contact:
mainnet.unichain.orgFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
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.
The full file from ccashwell/evm-cortex at commit f8f3301, republished under its MIT licence (© ccashwell). 582 words, ~8,160 tokens.
.claude/skills/uniswap-v4-testing/SKILL.md (or your agent's skills folder).The Deployers base contract from v4-core bootstraps a complete V4 environment — PoolManager, test routers, currencies, and standard constants.
// 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
);
}
}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) |
// Deploy PoolManager + all test routers
deployFreshManagerAndRouters();
// Deploy two sorted ERC-20 tokens, mint to address(this), approve all routers
deployMintAndApprove2Currencies();
// Deploy PoolManager only
deployFreshManager();Hook addresses encode permissions in their leading bits. HookMiner brute-forces a CREATE2 salt that produces an address with the correct bit pattern.
// 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");
}// 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
);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");
}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");
}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 |
Always set price limits to avoid reverts:
// 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 - 1function 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);
}
}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);
}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);
}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");
}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");
}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
);
}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");
}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);
}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");
}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)
}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
}
}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.
IPoolManager constant UNICHAIN_PM = IPoolManager(0x1F98400000000000000000000000000000000004);
function setUp() public {
vm.createSelectFork(vm.envString("UNICHAIN_RPC_URL"));
}# 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 5function 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`
}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 {}
}
}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);
}
}In foundry.toml:
[invariant]
runs = 256
depth = 64
fail_on_revert = falseHooks receive arbitrary bytes calldata hookData — test that your hook correctly parses and acts on it.
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
);
}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
);
}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/
├── 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 utilitiesabstract 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
);
}
}deployFreshManagerAndRouters() and deployMintAndApprove2Currencies() calledgetHookPermissions() matches the mined address bitsBaseHook.<function>.selectorZERO_BYTES without revertingCurrency.wrap(address(0)) works with hookforge snapshot (no universal threshold; review regressions with --check)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
Just SKILL.md in skills/uniswap-v4-testing of ccashwell/evm-cortex.
Open the folder on GitHubat commit f8f3301
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Uniswap V4 Testing this skillccashwell/evm-cortex | 131 | — | ~8.2k | Automated safety check: Pass | MIT | |
| Notebook To Strategytradingstrategy-ai/trade-executor | 160 | — | ~1.1k | Automated safety check: Pass | Custom licence | |
| Building Blocksaustintgriffith/ethskills | 295 | — | ~2.9k | Automated safety check: Pass | None | |
| Oracle Flashloan Analysisquillai-network/quillshield_skills | 130 | — | ~2.8k | Automated safety check: Pass | MIT | |
| Octav APIinternet-court/internet-court-skill | 6.5k | — | ~3.8k | Automated safety check: Pass | MIT | |
| Uniswap SwapNethereum/Nethereum | 2.3k | — | ~2.1k | Automated safety check: Pass | MIT |
tradingstrategy-ai/trade-executor
Transfer code from a backtesting Jupyter notebook to a Trade Executor strategy module
austintgriffith/ethskills
DeFi legos and protocol composability on Ethereum and L2s. An agent skill from austintgriffith/ethskills.
quillai-network/quillshield_skills
Detects price oracle manipulation and flash loan attack vectors in DeFi smart contracts.
internet-court/internet-court-skill
Integrate with Octav API for cryptocurrency portfolio tracking, transaction history, and DeFi analytics across 50+ blockchain networks.
Nethereum/Nethereum
Swap tokens on Uniswap V2/V3/V4 using Nethereum (.NET/C). An agent skill from Nethereum/Nethereum.
sickn33/agentic-awesome-skills
Build natural-language crypto/DeFi agents and EVM MCP plugins (Claude Code, Cursor, Codex, Gemini).
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.
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.
ccashwell/evm-cortex
Access control design patterns for Solidity protocols. An agent skill from ccashwell/evm-cortex.
ccashwell/evm-cortex
A skill your agent uses when running a local Ethereum node with Anvil.
ccashwell/evm-cortex
A skill your agent uses when performing systematic breadth-first review of all contracts during a security audit.
ccashwell/evm-cortex
A skill your agent uses when performing deep analysis of specific findings or high-risk areas during a security audit.
Works with
Categories
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.
Uniswap V4 Testing fits situations like: writing Foundry tests for Uniswap V4 hooks; router integrations; pool interactions.
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.
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.
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.
SKILL.md names no scripts, command-line tools or credentials: Uniswap V4 Testing is instructions for the agent only.
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.
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.
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.
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.
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.
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.