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 building on, integrating with, or analyzing Uniswap V4.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-expert --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-expert .claude/skills/uniswap-v4-expert && 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-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-expert into .claude/skills/uniswap-v4-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-expert", 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-expertType 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-expert -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-expert --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-expert .agents/skills/uniswap-v4-expert && 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-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-expert into .agents/skills/uniswap-v4-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-expert", 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-expert -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-expert --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-expert .cursor/skills/uniswap-v4-expert && 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-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-expert into .cursor/skills/uniswap-v4-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-expert", 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-expert--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-expert -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v4-expert --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-expert .gemini/skills/uniswap-v4-expert && 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-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-expert into .gemini/skills/uniswap-v4-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-expert", 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-expertInstalls 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-expert -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-expert .github/skills/uniswap-v4-expert && 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-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-expert into .github/skills/uniswap-v4-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-expert", 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-expert -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-expert --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-expert .opencode/skills/uniswap-v4-expert && 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-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v4-expert into .opencode/skills/uniswap-v4-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v4-expert", 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-expertA skill your agent uses when building on, integrating with, or analyzing Uniswap V4.
Uniswap V4 Expert is an agent skill from ccashwell/evm-cortex. Use when building on, integrating with, or analyzing Uniswap V4. Covers PoolManager singleton architecture, flash accounting via EIP-1153 transient storage, hook lifecycle, PoolKey structure, Currency type, dynamic fees, custom accounting, native ETH support, PositionManager (ERC-721 positions), and production deployment addresses.
Its SKILL.md is about 9.1k 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, covering Accounting and bookkeeping and Deployment. It works with Uniswap and Ethereum. The repository describes itself as: Ethereum protocol engineering squad for AI coding assistants. The licence is MIT.
7 steps, taken from the first numbered list in SKILL.md.
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.orgAlso links to:
developers.uniswap.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 Expert loads about 9.1k tokens when it runs. Until then it costs about 88 tokens; SKILL.md has 2,155 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). 2,155 words, ~9,105 tokens.
.claude/skills/uniswap-v4-expert/SKILL.md (or your agent's skills folder).Uniswap V4 replaces V3's factory-per-pool model with a singleton PoolManager — every pool lives inside a single contract. This eliminates redundant bytecode deployments and enables multi-hop swaps to settle only net token transfers. All state-changing operations use flash accounting via EIP-1153 transient storage: callers accumulate deltas during an unlock() callback and must zero out all balances before the callback returns.
┌────────────────────────────────────────────┐
│ PoolManager │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Pool A │ │ Pool B │ │ Pool C │ │
│ │ ETH/USDC │ │ WBTC/USDC │ │ ETH/DAI │ │
│ └──────────┘ └───────────┘ └──────────┘ │
│ │
│ Transient Storage (EIP-1153) │
│ ┌──────────────────────────────────────┐ │
│ │ currency → delta mapping (per lock) │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘poolManager.unlock(data)IUnlockCallback(msg.sender).unlockCallback(data)settle() (pay tokens in) and take() (withdraw tokens out)unlockCallback, PoolManager verifies all currency deltas are zeroCurrencyNotSettled()This means multi-hop swaps (e.g., A→B→C) only require net token movements for A and C, saving gas on intermediate transfers.
Functions callable outside unlock():
initialize() — creates a new pool (no balance changes)sync(currency) — snapshots reserves into transient storage; harmless outside a lockupdateDynamicLPFee() — called by hook contracts to set the current dynamic feeIProtocolFees admin functions (setProtocolFeeController, setProtocolFee, collectProtocolFees)swap, modifyLiquidity, donate, take, settle, settleFor, clear, mint, burn are all onlyWhenUnlocked and must run inside an active unlockCallback.
The unique identifier for a pool. Defined in v4-core/src/types/PoolKey.sol:
import {Currency} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
struct PoolKey {
/// @notice The lower currency of the pool, sorted numerically
Currency currency0;
/// @notice The higher currency of the pool, sorted numerically
Currency currency1;
/// @notice The pool LP fee, capped at 1_000_000. If the highest bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000
uint24 fee;
/// @notice Ticks that involve positions must be a multiple of tick spacing
int24 tickSpacing;
/// @notice The hooks of the pool
IHooks hooks;
}Sorting invariant: currency0 < currency1 is enforced. The PoolManager reverts with CurrenciesOutOfOrderOrEqual if violated. When constructing a PoolKey, always sort currencies by address value.
A bytes32 hash of the PoolKey, used as the storage key for pool state. Defined in v4-core/src/types/PoolId.sol:
type PoolId is bytes32;
library PoolIdLibrary {
function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) {
assembly ("memory-safe") {
// 0xa0 = 5 slots × 32 bytes (total size of PoolKey struct)
poolId := keccak256(poolKey, 0xa0)
}
}
}Usage: using PoolIdLibrary for PoolKey; then key.toId().
An address wrapper where address(0) represents native ETH. Defined in v4-core/src/types/Currency.sol:
type Currency is address;
library CurrencyLibrary {
Currency public constant ADDRESS_ZERO = Currency.wrap(address(0));
function isAddressZero(Currency currency) internal pure returns (bool) {
return Currency.unwrap(currency) == Currency.unwrap(ADDRESS_ZERO);
}
function transfer(Currency currency, address to, uint256 amount) internal { /* handles ETH vs ERC-20 */ }
function balanceOfSelf(Currency currency) internal view returns (uint256) { /* handles ETH vs ERC-20 */ }
}Native ETH pools use Currency.wrap(address(0)) as one of the currencies. No WETH wrapping required.
Two int128 values packed into a single int256. Upper 128 bits = amount0, lower 128 bits = amount1. Defined in v4-core/src/types/BalanceDelta.sol:
type BalanceDelta is int256;
library BalanceDeltaLibrary {
BalanceDelta public constant ZERO_DELTA = BalanceDelta.wrap(0);
function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) {
assembly ("memory-safe") {
_amount0 := sar(128, balanceDelta)
}
}
function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) {
assembly ("memory-safe") {
_amount1 := signextend(15, balanceDelta)
}
}
}Delta semantics from the caller's perspective:
settle())take())Return type of the beforeSwap hook. Upper 128 bits = delta in specified tokens, lower 128 bits = delta in unspecified tokens. Defined in v4-core/src/types/BeforeSwapDelta.sol:
type BeforeSwapDelta is int256;
function toBeforeSwapDelta(int128 deltaSpecified, int128 deltaUnspecified)
pure
returns (BeforeSwapDelta beforeSwapDelta)
{
assembly ("memory-safe") {
beforeSwapDelta := or(shl(128, deltaSpecified), and(sub(shl(128, 1), 1), deltaUnspecified))
}
}
library BeforeSwapDeltaLibrary {
BeforeSwapDelta public constant ZERO_DELTA = BeforeSwapDelta.wrap(0);
function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128);
function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128);
}Full interface from v4-core/src/interfaces/IPoolManager.sol. The PoolManager inherits IProtocolFees, IERC6909Claims, IExtsload, and IExttload.
function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);Creates a new pool. Does NOT require the unlock context. Reverts if currency0 >= currency1, if tickSpacing is zero or exceeds type(int16).max, or if the pool already exists. Emits Initialize event.
function unlock(bytes calldata data) external returns (bytes memory);Entry point for all delta-accounting operations. Calls IUnlockCallback(msg.sender).unlockCallback(data). After the callback returns, asserts all currency deltas are zero.
function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)
external
returns (BalanceDelta swapDelta);Executes a swap. Only callable inside unlockCallback. Invokes beforeSwap and afterSwap hooks if the pool's hook contract has those permissions.
function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData)
external
returns (BalanceDelta callerDelta, BalanceDelta feesAccrued);Adds or removes liquidity. Returns both the principal delta and fees accrued. A zero liquidityDelta "pokes" the position to collect fees without changing liquidity.
function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData)
external
returns (BalanceDelta);Distributes tokens to in-range liquidity providers. Useful for hook-driven fee distribution or protocol reward injection.
function settle() external payable returns (uint256 paid);
function settleFor(address recipient) external payable returns (uint256 paid);
function sync(Currency currency) external;
function take(Currency currency, address to, uint256 amount) external;
function clear(Currency currency, uint256 amount) external;settle(): Pays what the caller owes. For ERC-20 tokens, the caller must first call sync(currency), transfer tokens to the PoolManager, then call settle(). For native ETH, send value directly with settle{value: amount}(). Returns the amount credited.
sync(currency): Snapshots the PoolManager's current ERC-20 balance into transient storage. MUST be called before transferring ERC-20 tokens for settlement. Not needed for native ETH.
take(currency, to, amount): Withdraws tokens the PoolManager owes to the caller. Reverts if the caller's delta for that currency is insufficient.
clear(currency, amount): Zeros out a positive delta WITHOUT transferring tokens. The tokens are permanently locked in the PoolManager. Use only for dust amounts.
function mint(address to, uint256 id, uint256 amount) external;
function burn(address from, uint256 id, uint256 amount) external;Converts currency deltas into ERC-6909 claim tokens (and vice versa). The id is the currency address cast to uint256. Useful for holding balances inside the PoolManager across transactions without actual token transfers.
Defined in v4-core/src/types/PoolOperation.sol:
struct SwapParams {
/// Whether to swap token0 for token1 or vice versa
bool zeroForOne;
/// The desired input amount if negative (exactIn), or the desired output amount if positive (exactOut)
int256 amountSpecified;
/// The sqrt price at which, if reached, the swap will stop executing
uint160 sqrtPriceLimitX96;
}CRITICAL: amountSpecified sign convention:
Price limits:
zeroForOne = true: set sqrtPriceLimitX96 to a value less than the current price (price decreases)zeroForOne = false: set sqrtPriceLimitX96 to a value greater than the current price (price increases)TickMath.MIN_SQRT_PRICE + 1 or TickMath.MAX_SQRT_PRICE - 1 for unlimited slippageDefined in v4-core/src/types/PoolOperation.sol:
struct ModifyLiquidityParams {
int24 tickLower;
int24 tickUpper;
int256 liquidityDelta;
bytes32 salt;
}liquidityDelta > 0: add liquidityliquidityDelta < 0: remove liquidityliquidityDelta == 0: poke (collect accrued fees only)salt: differentiates multiple positions at the same tick range from the same addressSet at pool creation via PoolKey.fee. Denominated in hundredths of a basis point (1/100th of 1/10000th):
| PoolKey.fee | Effective Fee |
|---|---|
| 100 | 0.01% |
| 500 | 0.05% |
| 3000 | 0.30% |
| 10000 | 1.00% |
| 1000000 | 100% (MAX) |
From v4-core/src/libraries/LPFeeLibrary.sol:
library LPFeeLibrary {
uint24 public constant DYNAMIC_FEE_FLAG = 0x800000;
uint24 public constant OVERRIDE_FEE_FLAG = 0x400000;
uint24 public constant REMOVE_OVERRIDE_MASK = 0xBFFFFF;
uint24 public constant MAX_LP_FEE = 1000000; // 100%
}To create a dynamic fee pool, set PoolKey.fee = LPFeeLibrary.DYNAMIC_FEE_FLAG (exactly 0x800000).
Two mechanisms for dynamic fee updates:
Persistent update: Hook calls poolManager.updateDynamicLPFee(key, newFee) (e.g., in afterInitialize or periodically). This sets the stored fee for subsequent swaps.
Per-swap override: beforeSwap returns a fee with the override flag set in the third return value (uint24). The returned fee is desiredFee | LPFeeLibrary.OVERRIDE_FEE_FLAG. This overrides the stored fee for that single swap only.
import {SwapParams} from "v4-core/src/types/PoolOperation.sol";
// Inside a BaseHook subclass: override the internal _beforeSwap, not the external entry point
function _beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
uint24 dynamicFee = _computeFee();
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, dynamicFee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}Set by the protocolFeeController (appointed by the PoolManager owner via setProtocolFeeController) through IProtocolFees.setProtocolFee(PoolKey, uint24). The uint24 packs two direction-specific fees in pips: lower 12 bits = zeroForOne, upper 12 bits = oneForZero, each <= ProtocolFeeLibrary.MAX_PROTOCOL_FEE (1000 pips = 0.1%). The protocol fee is charged on the swap input first; the LP fee applies to the remainder (swapFee = protocolFee + lpFee - protocolFee * lpFee / 1e6).
Protocol fees are live on mainnet V4 pools since the governance proposal "Activate v4 Protocol Fees (Part 1/2)" executed on 2026-07-27. On Ethereum, PoolManager.owner() is the governance timelock 0x1a9C8182C09F50C8318d769245beA52c32BE35BC and protocolFeeController() is 0x89A5D5bF00a27D55c02951E49078a5C5771051dB; the native ETH/USDC 500/10 pool reports protocolFee = 512125 (125 pips in each direction). Fee-revenue models for hooks and LPs must account for this cut.
The PositionManager is the canonical periphery contract for managing liquidity positions as ERC-721 NFTs. Source: v4-periphery/src/PositionManager.sol.
contract PositionManager is
IPositionManager,
ERC721Permit_v4, // ERC-721 + EIP-4494 permit
PoolInitializer_v4,
Multicall_v4,
DeltaResolver,
ReentrancyLock,
BaseActionsRouter, // action dispatch via unlock
Notifier, // subscriber/notification pattern
Permit2Forwarder, // Permit2 integration
NativeWrapper // WETH wrapping/unwrapping
{ ... }NFT metadata: Name = "Uniswap v4 Positions NFT", Symbol = "UNI-V4-POSM".
function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;The standard entry point. Encodes a sequence of actions and their parameters. The unlockData is ABI-encoded as (bytes actions, bytes[] params) where actions is a packed byte array of action codes.
From v4-periphery/src/libraries/Actions.sol:
library Actions {
uint256 internal constant INCREASE_LIQUIDITY = 0x00;
uint256 internal constant DECREASE_LIQUIDITY = 0x01;
uint256 internal constant MINT_POSITION = 0x02;
uint256 internal constant BURN_POSITION = 0x03;
uint256 internal constant SWAP_EXACT_IN_SINGLE = 0x06;
uint256 internal constant SWAP_EXACT_IN = 0x07;
uint256 internal constant SWAP_EXACT_OUT_SINGLE = 0x08;
uint256 internal constant SWAP_EXACT_OUT = 0x09;
uint256 internal constant DONATE = 0x0a; // not supported by PositionManager or V4Router
uint256 internal constant SETTLE = 0x0b;
uint256 internal constant SETTLE_ALL = 0x0c;
uint256 internal constant SETTLE_PAIR = 0x0d;
uint256 internal constant TAKE = 0x0e;
uint256 internal constant TAKE_ALL = 0x0f;
uint256 internal constant TAKE_PORTION = 0x10;
uint256 internal constant TAKE_PAIR = 0x11;
uint256 internal constant CLOSE_CURRENCY = 0x12;
uint256 internal constant CLEAR_OR_TAKE = 0x13;
uint256 internal constant SWEEP = 0x14;
uint256 internal constant WRAP = 0x15;
uint256 internal constant UNWRAP = 0x16;
uint256 internal constant MINT_6909 = 0x17; // not supported by PositionManager or V4Router
uint256 internal constant BURN_6909 = 0x18; // not supported by PositionManager or V4Router
uint256 internal constant UNWIND_WITH_FALLBACK = 0x19;
}The library defines 26 constants (0x00–0x19); the two deprecated *_FROM_DELTAS codes are listed below.
DEPRECATED (vulnerable to sandwich attacks — lack slippage protection):
INCREASE_LIQUIDITY_FROM_DELTAS (0x04)MINT_POSITION_FROM_DELTAS (0x05)Mint a new position:
[MINT_POSITION, SETTLE_PAIR, SWEEP] // or CLOSE_CURRENCY for eachIncrease liquidity on existing position:
[INCREASE_LIQUIDITY, SETTLE_PAIR, SWEEP]Decrease liquidity and collect:
[DECREASE_LIQUIDITY, TAKE_PAIR]Burn an empty position:
[BURN_POSITION] // position must have zero liquidityThe Notifier base enables position subscribers — external contracts that receive callbacks when a position is modified. Subscribers implement ISubscriber:
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {PositionInfo} from "v4-periphery/src/libraries/PositionInfoLibrary.sol";
interface ISubscriber {
function notifySubscribe(uint256 tokenId, bytes memory data) external;
function notifyUnsubscribe(uint256 tokenId) external;
function notifyModifyLiquidity(uint256 tokenId, int256 liquidityChange, BalanceDelta feesAccrued) external;
function notifyBurn(uint256 tokenId, address owner, PositionInfo info, uint256 liquidity, BalanceDelta feesAccrued) external;
}Subscribe via positionManager.subscribe(tokenId, subscriber, data). The subscriber is notified on every liquidity modification or burn.
| Contract | Address |
|---|---|
| PoolManager | 0x000000000004444c5dc75cB358380D2e3dE08A90 |
| Universal Router (V2) | 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af |
| Universal Router 2.1.1 | 0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA |
| Universal Router 2.1.2 | 0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85 |
| PositionManager | 0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e |
| PositionDescriptor | 0xd1428Ba554F4C8450b763a0B2040A4935c63f06C |
| StateView | 0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227 |
| V4Quoter | 0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203 |
| ReservesLens | 0x0000001b173C3bbF3984D417d8614E3eed34865B |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| Contract | Address |
|---|---|
| PoolManager | 0x1F98400000000000000000000000000000000004 |
| PositionManager | 0x4529A01c7A0410167c5740C487A8DE60232617bf |
| StateView | 0x86e8631A016F9068C3f085fAF484Ee3F5fDee8f2 |
| V4Quoter | 0x333E3C607B141b18fF6de9f258db6e77fE7491E0 |
| Universal Router (V2) | 0xEf740bf23aCaE26f6492B10de645D6B98dC8Eaf3 |
| Universal Router 2.1.2 | 0xD1b797D92d87B688193A2B976eFc8D577D204343 |
RPC: https://mainnet.unichain.org. All Unichain addresses above were verified with cast code; the Ethereum PoolManager address has no code on Unichain. Unichain's protocolFeeController() is still address(0), so protocol fees are not yet active there.
V4 is deployed on 19 mainnets: Ethereum, Unichain, Optimism, Base, Arbitrum One, Polygon, Zora, Worldchain, X Layer, Ink, Soneium, Avalanche, BNB Smart Chain, Celo, Monad, MegaETH, Tempo, Robinhood Chain, Arc. Blast was removed from the official list (its onchain status is unverified).
CRITICAL: Addresses are NOT the same across chains. Always verify per-chain at https://developers.uniswap.org/docs/protocols/v4/deployments. Use cast code <address> --rpc-url <rpc> to confirm deployment before integrating.
A minimal router that performs a swap by implementing IUnlockCallback:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {SwapParams} from "v4-core/src/types/PoolOperation.sol";
import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol";
import {IERC20Minimal} from "v4-core/src/interfaces/external/IERC20Minimal.sol";
contract SimpleSwapRouter is IUnlockCallback {
using CurrencyLibrary for Currency;
using TransientStateLibrary for IPoolManager;
IPoolManager public immutable poolManager;
constructor(IPoolManager _poolManager) {
poolManager = _poolManager;
}
function swap(PoolKey calldata key, SwapParams calldata params) external payable {
poolManager.unlock(abi.encode(key, params, msg.sender));
}
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert();
(PoolKey memory key, SwapParams memory params, address sender) =
abi.decode(data, (PoolKey, SwapParams, address));
BalanceDelta delta = poolManager.swap(key, params, "");
_settleDelta(sender, key.currency0, delta.amount0());
_settleDelta(sender, key.currency1, delta.amount1());
return "";
}
function _settleDelta(address sender, Currency currency, int128 delta) internal {
if (delta < 0) {
// Caller owes tokens to PoolManager
uint256 amount = uint256(uint128(-delta));
if (currency.isAddressZero()) {
poolManager.settle{value: amount}();
} else {
poolManager.sync(currency);
IERC20Minimal(Currency.unwrap(currency)).transferFrom(sender, address(poolManager), amount);
poolManager.settle();
}
} else if (delta > 0) {
// PoolManager owes tokens to caller
poolManager.take(currency, sender, uint256(uint128(delta)));
}
}
}ERC-20 settlement:
1. poolManager.sync(currency) — snapshot current balance
2. token.transferFrom(user, poolManager) — transfer tokens in
3. poolManager.settle() — credit the delta
Native ETH settlement:
1. poolManager.settle{value: amount}() — send ETH directly
Withdrawal:
1. poolManager.take(currency, recipient, amount)For protocols that hold persistent balances in the PoolManager (avoiding repeated transfers):
// Convert positive delta to ERC-6909 claim tokens (keep balance in PM)
poolManager.mint(address(this), currency.toId(), amount);
// Later, burn claim tokens to create a negative delta (as if depositing)
poolManager.burn(address(this), currency.toId(), amount);forge install uniswap/v4-core
forge install uniswap/v4-periphery
forge install OpenZeppelin/uniswap-hooks # BaseHook (no longer in v4-periphery)[profile.default]
remappings = [
"v4-core/=lib/v4-core/",
"v4-periphery/=lib/v4-periphery/",
"@uniswap/v4-core/=lib/v4-core/",
"@uniswap/v4-periphery/=lib/v4-periphery/",
"@openzeppelin/uniswap-hooks/=lib/uniswap-hooks/",
"permit2/=lib/v4-periphery/lib/permit2/",
"forge-std/=lib/forge-std/src/",
]// Core types
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, BalanceDeltaLibrary} from "v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/src/types/PoolOperation.sol";
// Core interfaces
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
// Libraries
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol";
// Periphery — hooks (BaseHook no longer ships in v4-periphery; pick one)
import {BaseHook} from "@openzeppelin/uniswap-hooks/src/base/BaseHook.sol"; // v4-template default
// or: import {BaseHook} from "v4-hooks-public/src/base/BaseHook.sol"; // Uniswap Labs
// Periphery — position management
import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "v4-periphery/src/libraries/Actions.sol";// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {Deployers} from "v4-core/test/utils/Deployers.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
contract MyV4Test is Test, Deployers {
function setUp() public {
deployFreshManagerAndRouters();
deployMintAndApprove2Currencies();
(key,) = initPool(
currency0,
currency1,
IHooks(address(0)), // no hook
3000, // 0.30% fee
SQRT_PRICE_1_1 // 1:1 starting price
);
}
}The Deployers helper from v4-core/test/utils/Deployers.sol provides deployFreshManagerAndRouters(), deployMintAndApprove2Currencies(), initPool(), and test routers (swapRouter, modifyLiquidityRouter).
| Aspect | Uniswap V3 | Uniswap V4 |
|---|---|---|
| Architecture | Factory + individual pool contracts | Singleton PoolManager |
| Pool identification | Contract address | PoolKey → PoolId (bytes32 hash) |
| Token transfers | Direct transfers on every operation | Flash accounting (deltas in transient storage) |
| Multi-hop efficiency | Transfer tokens between each pool | Net settlement — only endpoints transfer |
| Native ETH | Must wrap to WETH first | Native ETH via Currency.wrap(address(0)) |
| Extensibility | No hook system | 10 hook callbacks (14 permission flags incl. 4 return-delta flags) |
| Fee model | Fixed fee tiers (0.01%, 0.05%, 0.30%, 1%) | Arbitrary static fees + dynamic fees via hooks |
| Fee distribution | Swap fees only | donate() for direct distribution to LPs |
| Position NFTs | NonfungiblePositionManager (V3) | PositionManager (still ERC-721) with Permit2 and batched Actions; ERC-6909 is for PoolManager claim tokens, not positions |
| LP fee updates | Immutable after pool creation | Dynamic via updateDynamicLPFee() |
| Transient storage | Not used (pre-Cancun) | EIP-1153 for delta tracking |
| Flash loans | Dedicated flash() function | Implicit via unlock — take first, settle later |
| Custom accounting | Not possible | Hooks can modify swap amounts via return deltas |
| Solidity version | 0.7.6 | 0.8.26 |
V4 has no dedicated flash loan function. Flash loans are implicit: inside unlockCallback, call take() to receive tokens, use them, then settle() to repay. As long as all deltas net to zero before the callback returns, the transaction succeeds. Effectively zero-fee flash loans.
function unlockCallback(bytes calldata) external returns (bytes memory) {
// Borrow 1000 USDC
poolManager.take(usdc, address(this), 1000e6);
// ... use the USDC (arbitrage, liquidation, etc.) ...
// Repay 1000 USDC
poolManager.sync(usdc);
IERC20(Currency.unwrap(usdc)).transfer(address(poolManager), 1000e6);
poolManager.settle();
return "";
}Hook addresses encode permissions in the leading bits of the address. The PoolManager validates these at pool initialization.
struct Permissions {
bool beforeInitialize;
bool afterInitialize;
bool beforeAddLiquidity;
bool afterAddLiquidity;
bool beforeRemoveLiquidity;
bool afterRemoveLiquidity;
bool beforeSwap;
bool afterSwap;
bool beforeDonate;
bool afterDonate;
bool beforeSwapReturnDelta;
bool afterSwapReturnDelta;
bool afterAddLiquidityReturnDelta;
bool afterRemoveLiquidityReturnDelta;
}function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96)
external returns (bytes4);
function afterInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, int24 tick)
external returns (bytes4);
function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, bytes calldata hookData)
external returns (bytes4);
function afterAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData)
external returns (bytes4, BalanceDelta);
function beforeRemoveLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, bytes calldata hookData)
external returns (bytes4);
function afterRemoveLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData)
external returns (bytes4, BalanceDelta);
function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
external returns (bytes4, BeforeSwapDelta, uint24);
function afterSwap(address sender, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta, bytes calldata hookData)
external returns (bytes4, int128);
function beforeDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1, bytes calldata hookData)
external returns (bytes4);
function afterDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1, bytes calldata hookData)
external returns (bytes4);When a hook has beforeSwapReturnDelta permission, the BeforeSwapDelta it returns modifies the swap:
deltaSpecified (upper 128 bits): adjusts the specified token amountdeltaUnspecified (lower 128 bits): adjusts the unspecified token amountWhen a hook has afterSwapReturnDelta permission, the int128 returned from afterSwap modifies the unspecified token delta.
Forgetting sync() before ERC-20 settlement: The PoolManager calculates payment by diffing its balance before and after. Without sync(), the diff is wrong.
amountSpecified sign confusion: Negative = exact input, positive = exact output. This is the reverse of what many developers expect.
Currency sorting: currency0 < currency1 is mandatory. Sort by address value before constructing PoolKey.
sqrtPriceLimitX96 direction: For zeroForOne = true, the limit must be BELOW current price. For zeroForOne = false, ABOVE. Using the wrong direction causes silent no-ops or reverts.
Hook address mismatch: Hook permission bits are encoded in the address itself. A hook deployed to the wrong address will fail validation at pool initialization.
Unchecked delta resolution: Every positive and negative delta MUST be resolved before unlockCallback returns. Partial resolution causes CurrencyNotSettled() revert.
Reentrancy through unlock: unlock() cannot be called while already unlocked (AlreadyUnlocked() error). Hooks cannot re-enter the PoolManager via a second unlock().
Fee precision: Fees are in hundredths of a bip (1e-6), NOT basis points. 3000 = 0.30%, not 30%.
Dynamic fee flag: A dynamic fee pool MUST set PoolKey.fee to exactly 0x800000. Any other value with the high bit set is invalid.
BalanceDelta packing: Don't cast BalanceDelta directly to int256 and interpret as a single number. Use .amount0() and .amount1() accessors.
currency0 < currency1)<= 1_000_000 or exactly DYNAMIC_FEE_FLAGtickSpacing > 0 and <= type(int16).maxswap, modifyLiquidity, donate, take, settle, settleFor, clear, mint, burn) runs inside an unlockCallback; only initialize, sync, updateDynamicLPFee, and the IProtocolFees admin functions are callable outsideunlockCallback validates msg.sender == address(poolManager)sync(currency) called before every ERC-20 transfer into PoolManagersettle{value: amount}() (not sync + transfer)take(), not left danglingclear() only for known-dust amounts (tokens are PERMANENTLY locked)amountSpecified sign is correct: negative = exact input, positive = exact outputsqrtPriceLimitX96 direction matches zeroForOne flagzeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1getHookPermissions() return valuebeforeSwap fee override includes LPFeeLibrary.OVERRIDE_FEE_FLAG<= LPFeeLibrary.MAX_LP_FEE (1_000_000)PoolId (not raw PoolKey) for gas efficiencyunlock() from within hook callbacksSETTLE_PAIR, TAKE_PAIR, etc.)SWEEP used to return excess tokens to the callerMINT_POSITION_FROM_DELTAS or INCREASE_LIQUIDITY_FROM_DELTASmsg.sender == address(poolManager) in unlockCallbackhookData from untrusted callers is validated or bounded in size© 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-expert of ccashwell/evm-cortex.
Open the folder on GitHubat commit f8f3301
Uniswap V4 Expert 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 Expert this skillccashwell/evm-cortex | 131 | — | ~9.1k | 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 | |
| Aomi Transactsickn33/agentic-awesome-skills | 47k | 1 repos | ~2.3k | Automated safety check: Pass | MIT | |
| Addressesaustintgriffith/ethskills | 295 | — | ~7.1k | Automated safety check: Pass | None | |
| Juicebox V6BankrBot/skills | 1.2k | — | ~1.5k | 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.
sickn33/agentic-awesome-skills
Build natural-language crypto/DeFi agents and EVM MCP plugins (Claude Code, Cursor, Codex, Gemini).
austintgriffith/ethskills
Verified contract addresses for major Ethereum protocols across mainnet and L2s.
BankrBot/skills
Build, inspect, configure, and safely transact with Juicebox V6 projects, terminals, rulesets, hooks, tiered NFTs, Revnets, Croptop, Bendystraw, and omnichain deployments.
LeoYeAI/openclaw-master-skills
Buy or sell XAUT (Tether Gold) on Ethereum. An agent skill from LeoYeAI/openclaw-master-skills.
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.
Categories
A skill your agent uses when building on, integrating with, or analyzing Uniswap V4. Uniswap V4 Expert is an agent skill from ccashwell/evm-cortex. Use when building on, integrating with, or analyzing Uniswap V4.
Uniswap V4 Expert fits situations like: integrating with; analyzing Uniswap V4.
Run `npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a claude-code`. Or copy the skill folder (skills/uniswap-v4-expert in ccashwell/evm-cortex) into .claude/skills/uniswap-v4-expert in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ccashwell/evm-cortex --skill uniswap-v4-expert -a codex`. Or copy the skill folder (skills/uniswap-v4-expert in ccashwell/evm-cortex) into .agents/skills/uniswap-v4-expert 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-expert -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-expert, .gemini/skills/uniswap-v4-expert, .github/skills/uniswap-v4-expert and .opencode/skills/uniswap-v4-expert in your project.
SKILL.md names no scripts, command-line tools or credentials: Uniswap V4 Expert is instructions for the agent only.
SKILL.md names 2 domains. In commands or code: mainnet.unichain.org; the agent is likely to contact it when it follows the instructions. As links in the text: developers.uniswap.org. 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 Expert is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.1k tokens (SKILL.md is roughly 36k 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 Expert: Notebook To Strategy (tradingstrategy-ai/trade-executor, 160 stars), Building Blocks (austintgriffith/ethskills, 295 stars), Aomi Transact (sickn33/agentic-awesome-skills, 47k stars) and Addresses (austintgriffith/ethskills, 295 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.