Uniswap V3 Swap
keypo-us/keypo-cli
A skill your agent uses when the user wants to swap tokens on Base Sepolia (or Base mainnet) using Uniswap V3.
A skill your agent uses when building on, integrating with, or analyzing Uniswap V3.
$ npx skills add ccashwell/evm-cortex --skill uniswap-v3-expert -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v3-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-v3-expert .claude/skills/uniswap-v3-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-v3-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v3-expert into .claude/skills/uniswap-v3-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v3-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-v3-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-v3-expert -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v3-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-v3-expert .agents/skills/uniswap-v3-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-v3-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v3-expert into .agents/skills/uniswap-v3-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v3-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-v3-expert -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v3-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-v3-expert .cursor/skills/uniswap-v3-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-v3-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v3-expert into .cursor/skills/uniswap-v3-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v3-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-v3-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-v3-expert -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ccashwell/evm-cortex uniswap-v3-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-v3-expert .gemini/skills/uniswap-v3-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-v3-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v3-expert into .gemini/skills/uniswap-v3-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v3-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-v3-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-v3-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-v3-expert .github/skills/uniswap-v3-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-v3-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v3-expert into .github/skills/uniswap-v3-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v3-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-v3-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-v3-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-v3-expert .opencode/skills/uniswap-v3-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-v3-expert" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/uniswap-v3-expert into .opencode/skills/uniswap-v3-expert/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "uniswap-v3-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-v3-expertA skill your agent uses when building on, integrating with, or analyzing Uniswap V3.
Uniswap V3 Expert is an agent skill from ccashwell/evm-cortex. Use when building on, integrating with, or analyzing Uniswap V3. Covers concentrated liquidity, tick-based pricing, UniswapV3Factory, UniswapV3Pool, NonfungiblePositionManager, SwapRouter, oracle observations, fee tiers, and production deployment addresses across all chains.
Its SKILL.md is about 9.6k 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 DevOps & Cloud. It works with Uniswap and Circle USDC. The repository describes itself as: Ethereum protocol engineering squad for AI coding assistants. The licence is MIT.
4 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 and bash).
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:
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 V3 Expert loads about 9.6k tokens when it runs. Until then it costs about 73 tokens; SKILL.md has 1,723 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). 1,723 words, ~9,555 tokens.
.claude/skills/uniswap-v3-expert/SKILL.md (or your agent's skills folder).Uniswap V3 is a concentrated liquidity AMM where each (tokenA, tokenB, fee) triple gets its own UniswapV3Pool contract, deployed via CREATE2 from a singleton UniswapV3Factory. LPs provide liquidity in discrete price ranges instead of across the full (0, infinity) curve, dramatically improving capital efficiency.
UniswapV3Factory (singleton)
├── creates UniswapV3Pool contracts via CREATE2 (one per token pair + fee tier)
│ ├── Core swap, mint, burn, collect, flash, observe logic
│ └── Stores tick state, positions, observations, and protocol fees
│
Periphery contracts (stateless routers / managers):
├── NonfungiblePositionManager — wraps LP positions as ERC-721 NFTs
├── SwapRouter (legacy v1) — single and multi-hop exact-input / exact-output swaps; Ethereum, Arbitrum, Optimism, Polygon only
├── SwapRouter02 — v2+v3 unified router with multicall
├── UniversalRouter — command-based router supporting v2, v3, permits, NFTs
├── Quoter — offchain swap simulation (reverts internally to return amounts)
├── QuoterV2 — returns sqrtPriceX96After, initializedTicksCrossed, gasEstimate
└── TickLens — batch read initialized ticks for a poolUniswap V3 enforces token0 < token1 (by address). The factory and pool reject misordered pairs. Always sort before calling factory or pool functions:
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);Pool addresses are deterministic. You can compute them offchain without querying the factory:
address pool = address(uint160(uint256(keccak256(abi.encodePacked(
hex"ff",
factory,
keccak256(abi.encode(token0, token1, fee)),
POOL_INIT_CODE_HASH
)))));The POOL_INIT_CODE_HASH for Uniswap V3 is:
0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54
Price is discretized into ticks. The price at tick i is:
P(i) = 1.0001^iThis gives ~1 basis point precision per tick. Ticks range from MIN_TICK = -887272 to MAX_TICK = 887272.
Uniswap V3 stores the square root of price as a Q64.96 fixed-point number:
sqrtPriceX96 = sqrt(token1 / token0) * 2^96Converting between tick and sqrtPriceX96:
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick);
int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);Boundary values:
TickMath.MIN_SQRT_RATIO = 4295128739 (tick -887272)TickMath.MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342 (tick 887272)| Fee (bps) | Fee (uint24) | Tick Spacing | Use Case |
|---|---|---|---|
| 0.01% | 100 | 1 | Stable pairs (USDC/USDT, DAI/USDC) |
| 0.05% | 500 | 10 | Stable pairs, correlated assets |
| 0.30% | 3000 | 60 | Standard pairs (ETH/USDC, WBTC/ETH) |
| 1.00% | 10000 | 200 | Exotic pairs, high volatility |
Tick spacing means LPs can only place range boundaries at ticks divisible by the spacing. The 1 bps tier was added via governance (not in original deployment).
Protocol fee is live on Ethereum mainnet. Since the UNIfication governance proposal executed on 2025-12-28, mainnet V3 pools carry a protocol fee: slot0().feeProtocol is 68 on the 0.01% and 0.05% tiers (protocol takes 1/4 of LP fees) and 102 on the 0.30% tier (1/6). Decode it as token0 = feeProtocol % 16, token1 = feeProtocol >> 4, protocol share = 1/N; the feeGrowthGlobal*X128 accumulators already accrue net of that share, so volume-based LP fee-revenue estimates must be multiplied by (1 - protocol_share).
Within a single tick range, the V3 pool behaves like a constant-product AMM scaled by liquidity L:
x * y = L^2 (virtual reserves within the active range)Real reserves required for a position between sqrtPriceA and sqrtPriceB with liquidity L:
amount0 = L * (1/sqrtPriceA - 1/sqrtPriceB) (when price < lower bound: all token0)
amount1 = L * (sqrtPriceB - sqrtPriceA) (when price > upper bound: all token1)When price is within the range, the position holds a mix of both tokens.
interface IUniswapV3Factory {
/// @notice Creates a pool for the given two tokens and fee
/// @param tokenA One of the two tokens in the desired pool
/// @param tokenB The other of the two tokens in the desired pool
/// @param fee The desired fee for the pool (100, 500, 3000, or 10000)
/// @return pool The address of the newly created pool
function createPool(
address tokenA,
address tokenB,
uint24 fee
) external returns (address pool);
/// @notice Returns the pool address for a given pair of tokens and fee, or address(0)
function getPool(
address tokenA,
address tokenB,
uint24 fee
) external view returns (address pool);
/// @notice Returns the tick spacing for a given fee amount
function feeAmountTickSpacing(uint24 fee) external view returns (int24);
/// @notice Returns the current protocol fee controller
function owner() external view returns (address);
/// @notice Enables a fee amount with the given tick spacing (governance only)
function enableFeeAmount(uint24 fee, int24 tickSpacing) external;
}interface IUniswapV3Pool {
/// @notice Sets the initial price for the pool. Can only be called once.
/// @param sqrtPriceX96 The initial sqrt price as a Q64.96 value
function initialize(uint160 sqrtPriceX96) external;
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position
/// @dev The caller of this method receives a callback (uniswapV3MintCallback)
/// in which they must pay any token0 or token1 owed for the liquidity
/// @param recipient The address for which the liquidity will be created
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param amount The amount of liquidity to mint
/// @param data Any data to be passed through to the callback
/// @return amount0 The amount of token0 that was paid to mint
/// @return amount1 The amount of token1 that was paid to mint
function mint(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount,
bytes calldata data
) external returns (uint256 amount0, uint256 amount1);
/// @notice Burns liquidity from the sender and accounts tokens owed
/// @dev Does NOT transfer tokens — must call collect() afterward
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param amount The amount of liquidity to burn
/// @return amount0 The amount of token0 owed to the position
/// @return amount1 The amount of token1 owed to the position
function burn(
int24 tickLower,
int24 tickUpper,
uint128 amount
) external returns (uint256 amount0, uint256 amount1);
/// @notice Collects tokens owed to a position
/// @dev Must burn(0) first to update fee accounting if only collecting fees
/// @param recipient The address which should receive the collected tokens
/// @param tickLower The lower tick of the position
/// @param tickUpper The upper tick of the position
/// @param amount0Requested How much token0 should be withdrawn
/// @param amount1Requested How much token1 should be withdrawn
/// @return amount0 The amount of token0 collected
/// @return amount1 The amount of token1 collected
function collect(
address recipient,
int24 tickLower,
int24 tickUpper,
uint128 amount0Requested,
uint128 amount1Requested
) external returns (uint128 amount0, uint128 amount1);
/// @notice Swap token0 for token1, or token1 for token0
/// @param recipient The address to receive the output of the swap
/// @param zeroForOne Direction: true = token0 → token1, false = token1 → token0
/// @param amountSpecified Positive = exact input, negative = exact output
/// @param sqrtPriceLimitX96 Price limit — swap stops if crossed
/// For zeroForOne: must be < current price and > MIN_SQRT_RATIO
/// For oneForZero: must be > current price and < MAX_SQRT_RATIO
/// @param data Callback data passed to uniswapV3SwapCallback
/// @return amount0 Delta of token0 balance of the pool (positive = pool received)
/// @return amount1 Delta of token1 balance of the pool
function swap(
address recipient,
bool zeroForOne,
int256 amountSpecified,
uint160 sqrtPriceLimitX96,
bytes calldata data
) external returns (int256 amount0, int256 amount1);
/// @notice Flash loans both tokens
/// @param recipient The address which will receive the token0 and token1 amounts
/// @param amount0 The amount of token0 to flash
/// @param amount1 The amount of token1 to flash
/// @param data Callback data passed to uniswapV3FlashCallback
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
/// @notice Returns cumulative tick and liquidity values at given seconds ago
/// @param secondsAgos Array of seconds ago from current block timestamp
/// @return tickCumulatives Cumulative tick values at each secondsAgo
/// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity
function observe(
uint32[] calldata secondsAgos
) external view returns (
int56[] memory tickCumulatives,
uint160[] memory secondsPerLiquidityCumulativeX128s
);
/// @notice Grows the observation buffer to support longer TWAPs
/// @param observationCardinalityNext Minimum number of observations to store
function increaseObservationCardinalityNext(
uint16 observationCardinalityNext
) external;
// --- State view functions ---
function slot0() external view returns (
uint160 sqrtPriceX96,
int24 tick,
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint8 feeProtocol,
bool unlocked
);
function liquidity() external view returns (uint128);
function fee() external view returns (uint24);
function token0() external view returns (address);
function token1() external view returns (address);
function tickSpacing() external view returns (int24);
function maxLiquidityPerTick() external view returns (uint128);
function ticks(int24 tick) external view returns (
uint128 liquidityGross,
int128 liquidityNet,
uint256 feeGrowthOutside0X128,
uint256 feeGrowthOutside1X128,
int56 tickCumulativeOutside,
uint160 secondsPerLiquidityOutsideX128,
uint32 secondsOutside,
bool initialized
);
function positions(bytes32 key) external view returns (
uint128 _liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
}Position keys for the core pool contract use keccak256(abi.encodePacked(owner, tickLower, tickUpper)).
The NonfungiblePositionManager (NPM) wraps core pool positions as ERC-721 NFTs. Most LPs interact with V3 through the NPM rather than calling the pool directly.
interface INonfungiblePositionManager {
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
/// @notice Creates a new position wrapped in an NFT
/// @return tokenId The ID of the minted NFT
/// @return liquidity The amount of liquidity for this position
/// @return amount0 The amount of token0 deposited
/// @return amount1 The amount of token1 deposited
function mint(MintParams calldata params)
external
payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
struct IncreaseLiquidityParams {
uint256 tokenId;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function increaseLiquidity(IncreaseLiquidityParams calldata params)
external
payable
returns (uint128 liquidity, uint256 amount0, uint256 amount1);
struct DecreaseLiquidityParams {
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
external
payable
returns (uint256 amount0, uint256 amount1);
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
/// @notice Collects fees and principal owed to a position
function collect(CollectParams calldata params)
external
payable
returns (uint256 amount0, uint256 amount1);
/// @notice Burns a token ID. Position must have 0 liquidity and 0 tokens owed.
function burn(uint256 tokenId) external payable;
/// @notice Returns the position data for a given token ID
function positions(uint256 tokenId) external view returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);
}To collect accrued trading fees without removing liquidity, call decreaseLiquidity with liquidity = 0 (or simply use collect after calling burn(0) on the core pool). Through the NPM:
// Trigger fee accounting update via zero-amount decrease
positionManager.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: 0,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
}));
// Collect all owed tokens (fees + any burned principal)
positionManager.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));// 1. Approve tokens to NPM
IERC20(token0).approve(address(positionManager), amount0);
IERC20(token1).approve(address(positionManager), amount1);
// 2. Mint position
(uint256 tokenId, uint128 liquidity, uint256 used0, uint256 used1) =
positionManager.mint(INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: 3000,
tickLower: -60,
tickUpper: 60,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0,
amount1Min: 0,
recipient: msg.sender,
deadline: block.timestamp
}));
// 3. Collect fees (anytime)
positionManager.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
// 4. Remove liquidity
positionManager.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
}));
// 5. Collect principal + remaining fees
positionManager.collect(INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: msg.sender,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
}));
// 6. Burn NFT (optional, position must have 0 liquidity and 0 owed)
positionManager.burn(tokenId);The original SwapRouter (0xE592427A0AEce92De3Edee1F18E0157C05861564) is a legacy contract: it is deployed only on Ethereum, Arbitrum One, Optimism and Polygon and is absent from Base, Unichain, BNB, Avalanche and Celo (on Base and Unichain that address holds an unrelated contract). The official docs name UniversalRouter as the preferred swap entrypoint, with SwapRouter02 as the V3-native alternative; the interface below is kept for reference and for reading existing integrations.
interface ISwapRouter {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96; // 0 for no limit
}
/// @notice Swaps amountIn of one token for as much as possible of another token
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);
struct ExactInputParams {
bytes path; // abi.encodePacked(tokenIn, fee, ..., tokenOut)
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
}
/// @notice Swaps along the specified multi-hop path
function exactInput(ExactInputParams calldata params)
external
payable
returns (uint256 amountOut);
struct ExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountOut;
uint256 amountInMaximum;
uint160 sqrtPriceLimitX96;
}
/// @notice Swaps as little as possible of one token for amountOut of another
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
returns (uint256 amountIn);
struct ExactOutputParams {
bytes path; // abi.encodePacked(tokenOut, fee, ..., tokenIn) — REVERSED
address recipient;
uint256 deadline;
uint256 amountOut;
uint256 amountInMaximum;
}
/// @notice Swaps along a reversed path to get exact output amount
function exactOutput(ExactOutputParams calldata params)
external
payable
returns (uint256 amountIn);
}Paths are tightly packed sequences of (token, fee, token, fee, ..., token):
// Single hop: WETH → 0.3% → USDC
bytes memory path = abi.encodePacked(WETH, uint24(3000), USDC);
// Multi-hop: WETH → 0.3% → USDC → 0.01% → DAI
bytes memory path = abi.encodePacked(WETH, uint24(3000), USDC, uint24(100), DAI);For exactOutput, the path is reversed (starts with output token):
// Exact output multi-hop: want DAI, pay WETH
// Path is: DAI → 0.01% → USDC → 0.3% → WETH (reversed order)
bytes memory path = abi.encodePacked(DAI, uint24(100), USDC, uint24(3000), WETH);SwapRouter02 at 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 on Ethereum mainnet unifies Uniswap V2 and V3 swaps under one interface with multicall support. The deadline parameter is removed from individual swap structs — use the checkDeadline multicall wrapper instead.
The Quoter simulates a swap and reverts internally to return the result. Never call it onchain (wastes gas with guaranteed revert).
interface IQuoterV2 {
struct QuoteExactInputSingleParams {
address tokenIn;
address tokenOut;
uint256 amountIn;
uint24 fee;
uint160 sqrtPriceLimitX96;
}
function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
external
returns (
uint256 amountOut,
uint160 sqrtPriceX96After,
uint32 initializedTicksCrossed,
uint256 gasEstimate
);
}Use QuoterV2 over the original Quoter — it returns additional data (sqrtPriceX96After, initializedTicksCrossed, gasEstimate) useful for routing.
Every V3 pool stores an array of (blockTimestamp, tickCumulative, secondsPerLiquidityCumulative) observations. The pool writes one observation per block in which a swap occurs.
// Get the 30-minute TWAP tick
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800; // 30 minutes ago
secondsAgos[1] = 0; // now
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int24 twapTick = int24((tickCumulatives[1] - tickCumulatives[0]) / 1800);
// Convert tick to price
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(twapTick);increaseObservationCardinalityNext() to enable longer TWAPs// Expand observation buffer to support 1-hour TWAP
pool.increaseObservationCardinalityNext(350); // some bufferV3 uses a pull-based token collection model. The pool calls back into the caller to collect tokens owed.
/// @notice Called by the pool after executing a swap
/// @dev Must pay the pool the tokens owed for the swap.
/// Positive delta = tokens owed TO the pool.
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata data
) external {
// CRITICAL: Verify the caller is the expected pool
require(msg.sender == address(pool), "unauthorized callback");
// Decode any data passed through the swap
address payer = abi.decode(data, (address));
// Pay whichever token is owed (positive delta = owed to pool)
if (amount0Delta > 0) {
IERC20(pool.token0()).safeTransferFrom(payer, msg.sender, uint256(amount0Delta));
}
if (amount1Delta > 0) {
IERC20(pool.token1()).safeTransferFrom(payer, msg.sender, uint256(amount1Delta));
}
}/// @notice Called by the pool when minting liquidity
/// @dev Must pay the pool both token0 and token1 owed
function uniswapV3MintCallback(
uint256 amount0Owed,
uint256 amount1Owed,
bytes calldata data
) external {
require(msg.sender == address(pool), "unauthorized callback");
address payer = abi.decode(data, (address));
if (amount0Owed > 0) {
IERC20(pool.token0()).safeTransferFrom(payer, msg.sender, amount0Owed);
}
if (amount1Owed > 0) {
IERC20(pool.token1()).safeTransferFrom(payer, msg.sender, amount1Owed);
}
}/// @notice Called by the pool after a flash loan
/// @dev Must repay the flash-loaned amount plus fees
function uniswapV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external {
require(msg.sender == address(pool), "unauthorized callback");
// Perform arbitrage or other operations here
// Repay principal + fee
if (fee0 > 0 || amount0 > 0) {
IERC20(pool.token0()).safeTransfer(msg.sender, amount0 + fee0);
}
if (fee1 > 0 || amount1 > 0) {
IERC20(pool.token1()).safeTransfer(msg.sender, amount1 + fee1);
}
}Callback security: Always verify msg.sender is the expected pool. Compute the expected pool address via CREATE2 rather than storing it, or validate against the factory:
function _verifyCallback(address tokenA, address tokenB, uint24 fee) internal view {
address expected = IUniswapV3Factory(factory).getPool(tokenA, tokenB, fee);
require(msg.sender == expected, "unauthorized callback");
}| Contract | Address |
|---|---|
| UniswapV3Factory | 0x1F98431c8aD98523631AE4a59f267346ea31F984 |
| NonfungiblePositionManager | 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 |
| SwapRouter (legacy v1) | 0xE592427A0AEce92De3Edee1F18E0157C05861564 |
| SwapRouter02 | 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 |
| Quoter | 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6 |
| QuoterV2 | 0x61fFE014bA17989E743c5F6cB21bF9697530B21e |
| UniversalRouter V2 | 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af |
| UniversalRouter V2.1.1 | 0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA |
| UniversalRouter V2.1.2 | 0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85 |
| TickLens | 0xbfd8137f7d1516D3ea5cA83523914859ec47F573 |
The original CREATE2 set (Factory 0x1F98…F984, NPM 0xC364…FE88, SwapRouter 0xE592…1564, SwapRouter02 0x68b3…Fc45, QuoterV2 0x61fF…B21e) is shared only by Ethereum, Arbitrum One, Optimism and Polygon (verified by cast code); every later chain has its own addresses:
| Chain | Factory | NPM | SwapRouter02 | QuoterV2 |
|---|---|---|---|---|
| Arbitrum | 0x1F98431c...F984 | 0xC36442b4...FE88 | 0x68b34658...Fc45 | 0x61fFE014...B21e |
| Optimism | 0x1F98431c...F984 | 0xC36442b4...FE88 | 0x68b34658...Fc45 | 0x61fFE014...B21e |
| Polygon | 0x1F98431c...F984 | 0xC36442b4...FE88 | 0x68b34658...Fc45 | 0x61fFE014...B21e |
| Base | 0x33128a8fC17869897dcE68Ed026d694621f6FDfD | 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1 | 0x2626664c2603336E57B271c5C0b26F421741e481 | 0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a |
| Unichain | 0x1F98400000000000000000000000000000000003 | 0x943e6e07a7E8E791dAFC44083e54041D743C46E9 | 0x73855d06DE49d0fe4A9c42636Ba96c62da12FF9C | 0x385A5cf5F83e99f7BB2852b6A19C3538b9FA7658 |
| BNB Chain | 0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7 | 0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613 | 0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2 | 0x78D78E420Da98ad378D7799bE8f4AF69033EB077 |
| Avalanche | 0x740b1c1de25031C31FF4fC9A62f554A55cdC1baD | 0x655C406EBFa14EE2006250925e54ec43AD184f8B | 0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE | 0xbe0F5544EC67e9B3b2D979aaA43f18Fd87E6257F |
| Celo | 0xAfE208a311B21f13EF87E33A90049fC17A7acDEc | 0x3d79EdAaBC0EaB6F08ED885C05Fc0B014290D95A | 0x5615CDAb10dc425a742d643d949a7F474C01abc4 | 0x82825d0554fA07f7FC52Ab63c961F330fdEFa8E8 |
CRITICAL: Addresses on Base, BNB, Avalanche, Celo, and newer chains differ from the canonical set. Always verify with cast code <address> --rpc-url <chain> or check the official per-chain deployment pages at https://developers.uniswap.org/docs/protocols/v3/deployments/<chain> (the former docs.uniswap.org URLs redirect there) before integrating.
forge install Uniswap/v3-core
forge install Uniswap/v3-periphery
forge install OpenZeppelin/openzeppelin-contracts@uniswap/v3-core/=lib/v3-core/
@uniswap/v3-periphery/=lib/v3-periphery/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/This test targets the legacy SwapRouter v1, so it only works on Ethereum, Arbitrum One, Optimism and Polygon forks. To target SwapRouter02 instead (0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 on Ethereum; per-chain addresses in the table above), use IV3SwapRouter from Uniswap/swap-router-contracts — its ExactInputSingleParams has no deadline field, so drop that line and wrap the call in multicall(uint256 deadline, bytes[] data) if you need a deadline.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
contract UniswapV3ForkTest is Test {
ISwapRouter constant ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function setUp() public {
vm.createSelectFork("mainnet");
}
function test_exactInputSingle() public {
uint256 amountIn = 1 ether;
deal(WETH, address(this), amountIn);
IERC20(WETH).approve(address(ROUTER), amountIn);
uint256 amountOut = ROUTER.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: USDC,
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
assertGt(amountOut, 0, "swap returned zero");
assertGt(IERC20(USDC).balanceOf(address(this)), 0, "no USDC received");
}
}Run with:
forge test --fork-url $ETH_RPC_URL --match-contract UniswapV3ForkTest -vvvFor maximum gas efficiency or custom routing, call the pool directly:
// Swap 1 WETH for USDC via the WETH/USDC 0.3% pool
IUniswapV3Pool pool = IUniswapV3Pool(factory.getPool(WETH, USDC, 3000));
// zeroForOne = true means token0 → token1
// amountSpecified > 0 means exact input
(int256 amount0, int256 amount1) = pool.swap(
recipient,
true, // zeroForOne
int256(1 ether), // exact input
TickMath.MIN_SQRT_RATIO + 1, // price limit (min for zeroForOne)
abi.encode(msg.sender) // callback data
);// For zeroForOne swaps (token0 → token1), price decreases
uint160 sqrtPriceLimitX96 = TickMath.MIN_SQRT_RATIO + 1;
// For oneForZero swaps (token1 → token0), price increases
uint160 sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1;// Read position from NPM
(, , , , , int24 tickLower, int24 tickUpper, uint128 liquidity,
uint256 feeGrowthInside0Last, uint256 feeGrowthInside1Last,
uint128 tokensOwed0, uint128 tokensOwed1) = npm.positions(tokenId);
// Read current fee growth from pool
(uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) =
(pool.feeGrowthGlobal0X128(), pool.feeGrowthGlobal1X128());
// Calculate uncollected fees (simplified — needs tick fee growth subtraction)
uint256 fees0 = tokensOwed0 + (feeGrowthInside0Current - feeGrowthInside0Last) * liquidity / (1 << 128);
uint256 fees1 = tokensOwed1 + (feeGrowthInside1Current - feeGrowthInside1Last) * liquidity / (1 << 128);JIT liquidity is a MEV strategy where a searcher mints a tight range position just before a large swap, earns concentrated fees, then removes the position in the same or next block:
Block N:
1. Observe pending large swap in mempool
2. Mint concentrated position around current tick
3. Large swap executes — LP earns concentrated fees
4. Remove liquidity and collect feesThis is profitable because concentrated liquidity in a tiny range captures nearly all fees from the swap. However, it requires mempool access and priority gas auctions.
| Aspect | Uniswap V3 | Uniswap V4 |
|---|---|---|
| Pool architecture | One contract per pool | Singleton PoolManager |
| Pool creation | factory.createPool() | poolManager.initialize() |
| Token transfers | Callbacks (pull pattern) | Flash accounting with settle() / take() |
| LP positions | NonfungiblePositionManager (ERC-721) | PositionManager (ERC-721, Permit2, action-batched) |
| Swap routing | SwapRouter / SwapRouter02 | UniversalRouter or custom routers |
| Extensibility | None | Hooks at every lifecycle point |
| Fee model | Fixed fee tiers | Dynamic fees via hooks |
| Flash loans | pool.flash() | Free via flash accounting (take + settle within unlock) |
| Pool ID | Contract address | keccak256(abi.encode(PoolKey)) |
| Gas (single swap) | ~130k-180k | ~100k-140k (singleton savings) |
| Oracle | Built-in observation array | Removed from core — implement via hooks |
PositionManager mints ERC-721 NFTs ("Uniswap v4 Positions NFT") but is driven by batched Actions via modifyLiquidities; ERC-6909 in V4 is the PoolManager's claim-token standard (mint/burn), not LP positions.settle() and take() within an unlock() context.Always set meaningful amountOutMinimum (exact input) or amountInMaximum (exact output). Using 0 invites sandwich attacks.
// Calculate minimum output with 0.5% slippage tolerance
uint256 amountOutMin = (expectedAmountOut * 995) / 1000;Always use a reasonable deadline parameter. Setting block.timestamp is useless onchain (always passes). Use block.timestamp + 300 (5 minutes) or pass a user-specified deadline.
Unverified callbacks are the most common V3 integration vulnerability. Always verify:
// Option A: Check against factory
require(msg.sender == IUniswapV3Factory(FACTORY).getPool(token0, token1, fee));
// Option B: Recompute CREATE2 address (saves an external call)
address expected = address(uint160(uint256(keccak256(abi.encodePacked(
hex"ff",
FACTORY,
keccak256(abi.encode(token0, token1, fee)),
POOL_INIT_CODE_HASH
)))));
require(msg.sender == expected);slot0().sqrtPriceX96 as a price oracle — it reflects the instantaneous spot price and is trivially manipulable within a transactionobserve() for TWAP over multiple blocksV3 pools have a slot0.unlocked mutex that prevents reentrancy into the pool during swaps and mints. However, your own contracts receiving callbacks should still use nonReentrant guards for defense in depth.
SafeERC20 for all token transfersamountOutMinimum / amountInMaximum set to meaningful values (never 0 in production)block.timestamp)msg.sender is the expected pool via CREATE2 or factory lookuptoken, fee, token, ...)exactOutput, path is reversed (output token first)token0 < token1)tickSpacingamount0Min / amount1Min protect against sandwich attacks during mintdecreaseLiquidity(0) or equivalent to update accountingburnslot0().sqrtPriceX96 as a price oracleOLD observation errors (requested time exceeds oldest observation)msg.sender is the poolamount + fee)forge test --fork-urlforge test --gas-report© 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-v3-expert of ccashwell/evm-cortex.
Open the folder on GitHubat commit f8f3301
Uniswap V3 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 V3 Expert this skillccashwell/evm-cortex | 131 | — | ~9.6k | Automated safety check: Pass | MIT | |
| Uniswap V3 Swapkeypo-us/keypo-cli | 182 | — | ~3.2k | Automated safety check: Pass | MIT | |
| Okx Dapp Discoveryinternet-court/internet-court-skill | 6.6k | 1 repos | ~6.9k | Automated safety check: Pass | MIT | |
| Trading Evmalsk1992/CloddsBot | 3k | — | ~1.9k | Automated safety check: Pass | MIT | |
| Cortx Reliabilityaeonfun/aeon | 770 | — | ~1.1k | Automated safety check: Pass | MIT | |
| Okx Dapp Discoveryaiskillstore/marketplace | 433 | 1 repos | ~5.6k | Automated safety check: Pass | MIT |
keypo-us/keypo-cli
A skill your agent uses when the user wants to swap tokens on Base Sepolia (or Base mainnet) using Uniswap V3.
internet-court/internet-court-skill
Plugin router for 20 third-party DeFi protocols (Polymarket, Aave, Hyperliquid, PancakeSwap, Morpho, Raydium, Curve, Compound, Pendle, Lido, ether.fi, GMX, Kamino, Orca, Meteora, Clanker, pump.fun…
alsk1992/CloddsBot
Trade tokens on EVM chains - Uniswap V3, 1inch on Ethereum, Arbitrum, Optimism, Base, Polygon
aeonfun/aeon
Check whether an x402 payment endpoint is reliably delivering value before spending USDC on it.
aiskillstore/marketplace
Plugin router for 20 third-party DeFi protocols (Polymarket, Aave, Hyperliquid, PancakeSwap, Morpho, Raydium, Curve, Compound, Pendle, Lido, ether.fi, GMX, Kamino, Orca, Meteora, Clanker, pump.fun…
austintgriffith/ethskills
Verified contract addresses for major Ethereum protocols across mainnet and L2s.
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 building on, integrating with, or analyzing Uniswap V3. Uniswap V3 Expert is an agent skill from ccashwell/evm-cortex. Use when building on, integrating with, or analyzing Uniswap V3.
Uniswap V3 Expert fits situations like: integrating with; analyzing Uniswap V3.
Run `npx skills add ccashwell/evm-cortex --skill uniswap-v3-expert -a claude-code`. Or copy the skill folder (skills/uniswap-v3-expert in ccashwell/evm-cortex) into .claude/skills/uniswap-v3-expert in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ccashwell/evm-cortex --skill uniswap-v3-expert -a codex`. Or copy the skill folder (skills/uniswap-v3-expert in ccashwell/evm-cortex) into .agents/skills/uniswap-v3-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-v3-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-v3-expert, .gemini/skills/uniswap-v3-expert, .github/skills/uniswap-v3-expert and .opencode/skills/uniswap-v3-expert in your project.
SKILL.md names no scripts, command-line tools or credentials: Uniswap V3 Expert is instructions for the agent only.
SKILL.md names 1 domain. In commands or code: developers.uniswap.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 V3 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.6k tokens (SKILL.md is roughly 38k 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 V3 Expert: Uniswap V3 Swap (keypo-us/keypo-cli, 182 stars), Okx Dapp Discovery (internet-court/internet-court-skill, 6.6k stars), Trading Evm (alsk1992/CloddsBot, 3k stars) and Cortx Reliability (aeonfun/aeon, 770 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.