--- name: uniswap-v4-hooks description: | Secure Uniswap v4 hook development assistant. Activates when: - Working with Uniswap v4 hooks, PoolManager, or hook contracts - Keywords: uniswap, v4, hook, hooks, beforeSwap, afterSwap, PoolManager, IHooks, BaseHook - Creating, reviewing, or auditing hook contracts - Implementing custom AMM logic, dynamic fees, or swap modifications - User says "uniswap hooks" or invokes "/uniswap-v4-hooks" --- This skill guides secure development of Uniswap v4 hooks. Hooks are external smart contracts that intercept pool operations at specific lifecycle points. Security is paramount—hook vulnerabilities can drain user funds. ## Security Thinking Before writing ANY hook code, assess the threat model: **Who calls your hook?** - Only `PoolManager` should call hook functions - `msg.sender` in a hook is ALWAYS `PoolManager`, never the user - The `sender` parameter is the router, not the end user **What state is exposed?** - Hooks execute mid-transaction—state can be manipulated between callbacks - Reentrancy is possible through external calls - Shared storage across pools using the same hook can break unexpectedly **What can go wrong with deltas?** - Every token in MUST equal tokens out (delta accounting) - Rounding errors accumulate in iterative operations - BeforeSwapDelta can bypass normal swap logic entirely ## CRITICAL: The NoOp Rug Pull Vector Hooks with `BEFORE_SWAP_RETURNS_DELTA_FLAG` can **steal user funds**. This is the most dangerous hook permission. ```solidity // MALICIOUS EXAMPLE - DO NOT USE // This hook takes user tokens and returns nothing function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) { Currency input = params.zeroForOne ? key.currency0 : key.currency1; // Take all user funds input.take(poolManager, address(this), uint256(-params.amountSpecified), false); // Return delta saying we took everything, returning zero return ( BaseHook.beforeSwap.selector, toBeforeSwapDelta(int128(-params.amountSpecified), 0), // THEFT 0 ); } ``` **When `beforeSwapReturnDelta: true`:** - The hook can completely replace swap logic - Pool math is SKIPPED if delta equals amountSpecified - User funds flow to the hook, not through the AMM curve **Defense:** NEVER enable `beforeSwapReturnDelta` unless implementing a legitimate custom AMM. Users should verify hook permissions before swapping. ## Permission Flags (Address Encoding) Hook permissions are encoded in the contract address. The address must have specific bits set: | Bit | Permission | Risk Level | |-----|------------|------------| | 0 | beforeInitialize | Low | | 1 | afterInitialize | Low | | 2 | beforeAddLiquidity | Medium | | 3 | afterAddLiquidity | Medium | | 4 | beforeRemoveLiquidity | Medium | | 5 | afterRemoveLiquidity | Medium | | 6 | beforeSwap | High | | 7 | afterSwap | Medium | | 8 | beforeDonate | Low | | 9 | afterDonate | Low | | 10 | beforeSwapReturnDelta | **CRITICAL** | | 11 | afterSwapReturnDelta | High | | 12 | afterAddLiquidityReturnDelta | Medium | | 13 | afterRemoveLiquidityReturnDelta | Medium | **Address Mining:** Use CREATE2 with salt grinding to deploy at an address with correct permission bits. Tools: `hook-mine-and-sinker`, `v4-hook-address-miner`. ## Access Control Pattern ```solidity // REQUIRED: Verify caller is PoolManager modifier onlyPoolManager() { require(msg.sender == address(poolManager), "Not PoolManager"); _; } function beforeSwap( address sender, // This is the ROUTER, not the user PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata hookData ) external onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) { // ... } ``` ## Identifying the Actual User (msg.sender Problem) The `sender` parameter is the router contract, NOT the end user. To get the actual user: ```solidity // 1. Define interface for trusted routers interface IMsgSender { function msgSender() external view returns (address); } // 2. Maintain allowlist of trusted routers mapping(address => bool) public trustedRouters; // 3. Query router safely in hook function afterSwap( address sender, // This is the router PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta delta, bytes calldata hookData ) external override returns (bytes4, int128) { // CRITICAL: Only trust verified routers if (!trustedRouters[sender]) { revert UntrustedRouter(sender); } // Safe to query actual user try IMsgSender(sender).msgSender() returns (address user) { // Use `user` for rewards, tracking, etc. } catch { revert RouterDoesNotImplementMsgSender(); } return (this.afterSwap.selector, 0); } ``` **NEVER trust `tx.origin`** for authentication (only acceptable for anti-gaming checks like self-referral prevention). ## Delta Accounting Rules Deltas track what the hook owes or is owed. They MUST net to zero. ```solidity // Taking tokens FROM PoolManager (hook receives tokens) currency.take(poolManager, address(this), amount, false); // Settling tokens TO PoolManager (hook sends tokens) currency.settle(poolManager, address(this), amount, false); // INVARIANT: All deltas must balance before unlock completes ``` **Common Delta Bugs:** - Forgetting to settle after taking - Rounding in wrong direction (always round against the user/hook) - Not handling both swap directions (zeroForOne true AND false) ## Hook Implementation Checklist Before ANY hook implementation: - [ ] **Access Control**: Only PoolManager can call hook functions - [ ] **Delta Balance**: Every take has a corresponding settle - [ ] **Router Verification**: Never trust sender without allowlist check - [ ] **Overflow Protection**: Use `mulDiv` for price math, never raw multiplication - [ ] **Reentrancy Guards**: Add if making external calls - [ ] **Token Type Handling**: Document unsupported tokens (rebasing, fee-on-transfer) - [ ] **Permission Flags**: Minimal permissions needed for functionality ## Base Hook Template ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; import {PoolKey} from "v4-core/types/PoolKey.sol"; import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; import {Hooks} from "v4-core/libraries/Hooks.sol"; contract MyHook is BaseHook { constructor(IPoolManager _manager) BaseHook(_manager) {} function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeAddLiquidity: false, afterAddLiquidity: false, beforeRemoveLiquidity: false, afterRemoveLiquidity: false, beforeSwap: false, afterSwap: true, // Enable only what you need beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, // DANGEROUS - enable with caution afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); } function _afterSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta delta, bytes calldata hookData ) internal override returns (bytes4, int128) { // Your logic here return (this.afterSwap.selector, 0); } } ``` ## Common Vulnerability Patterns ### 1. Overflow in Price Calculations ```solidity // BAD: Can overflow uint256 price = uint256(sqrtPriceX96) ** 2; // GOOD: Use mulDiv uint256 price = FullMath.mulDiv(uint256(sqrtPriceX96), uint256(sqrtPriceX96), 1 << 96); ``` ### 2. Incorrect Fee Calculation ```solidity // BAD: Simple addition is wrong uint256 totalFee = protocolFee + lpFee; // GOOD: Protocol fee taken first, then LP fee from remainder // protocolFee + lpFee * (1_000_000 - protocolFee) / 1_000_000 ``` ### 3. Missing Timestamp Validation ```solidity // BAD: No future check function submitOrder(uint256 expiration) external { orders[msg.sender] = Order(expiration, ...); } // GOOD: Validate expiration is in future function submitOrder(uint256 expiration) external { require(expiration > block.timestamp, "Expiration must be in future"); require(expiration % interval == 0, "Must align to interval"); orders[msg.sender] = Order(expiration, ...); } ``` ### 4. Trusting Sender Without Verification ```solidity // BAD: sender could be malicious contract address user = sender; // GOOD: Verify router and query actual user require(trustedRouters[sender], "Untrusted router"); address user = IMsgSender(sender).msgSender(); ``` ## Token Handling Hazards **Unsupported token types** (document explicitly if your hook doesn't handle): - **Fee-on-transfer**: Actual received amount differs from transfer amount - **Rebasing**: Balance changes without transfers - **ERC-777**: Reentrancy via transfer hooks - **Pausable**: Transfers can revert unexpectedly - **Blocklist**: Some addresses may be blocked ```solidity // If handling non-standard tokens, validate actual balances uint256 balanceBefore = token.balanceOf(address(this)); token.transferFrom(user, address(this), amount); uint256 actualReceived = token.balanceOf(address(this)) - balanceBefore; ``` ## Testing Requirements Use Foundry for comprehensive testing: ```solidity // Invariant test: Deltas always balance function invariant_deltasBalance() public { assertEq(poolManager.currencyDelta(address(hook), currency0), 0); assertEq(poolManager.currencyDelta(address(hook), currency1), 0); } // Fuzz test: No overflow in price calculations function testFuzz_priceCalculation(uint160 sqrtPriceX96) public { vm.assume(sqrtPriceX96 >= TickMath.MIN_SQRT_PRICE); vm.assume(sqrtPriceX96 <= TickMath.MAX_SQRT_PRICE); // Should not revert hook.calculatePrice(sqrtPriceX96); } // Test both swap directions function test_swapZeroForOne() public { ... } function test_swapOneForZero() public { ... } ``` ## Risk Assessment (Self-Score) Before deployment, score your hook (0-33 scale): | Dimension | Score Range | Your Hook | |-----------|-------------|-----------| | Code Complexity | 0-5 | | | Custom Math | 0-5 | | | External Dependencies | 0-3 | | | External Liquidity | 0-3 | | | TVL Potential | 0-5 | | | Team Maturity | 0-3 | | | Upgradeability | 0-3 | | | Autonomous Updates | 0-3 | | | Price-Impacting | 0-3 | | **Risk Tiers:** - **Low (0-6)**: 1 audit + AI static analysis - **Medium (7-17)**: 1-2 audits + bug bounty recommended - **High (18-33)**: 2 audits (1 math specialist) + mandatory bug bounty + monitoring ## Production Hooks Reference Allowlisted hooks on Uniswap (as of Jan 2025): - Flaunch (meme coin launchpad, 100% fees to creators) - EulerSwap (lending-powered DEX) - Zaha Studios TWAMM (time-weighted orders) - Coinbase Verified Pools - Panoptic Oracle Hook - AEGIS Dynamic Fee Mechanism Study these for patterns: https://raw.githubusercontent.com/fewwwww/awesome-uniswap-hooks/refs/heads/main/README.md ## NEVER Do This - NEVER enable `beforeSwapReturnDelta` without understanding the rug pull risk - NEVER trust `sender` parameter without router verification - NEVER use `unchecked` blocks for price/amount math - NEVER assume ERC20 standard behavior for all tokens - NEVER skip delta settlement - NEVER deploy without address permission verification - NEVER store sensitive state readable mid-swap - NEVER use `tx.origin` for authentication ## Resources - **v4-core**: https://github.com/Uniswap/v4-core - **v4-periphery**: https://github.com/Uniswap/v4-periphery - **Security Framework**: https://docs.uniswap.org/contracts/v4/security - **Hook Examples**: https://github.com/fewwwww/awesome-uniswap-hooks - **Address Mining**: https://github.com/hensha256/hook-mine-and-sinker - **v4 by Example**: https://v4-by-example.org ## Audit Checklist (Pre-Deployment) - [ ] All hook functions check `msg.sender == poolManager` - [ ] Deltas verified to net zero in all paths - [ ] No overflow possible in math operations - [ ] Router allowlist implemented if identifying users - [ ] Token types explicitly documented - [ ] Reentrancy guards on external calls - [ ] Timestamp validations in place - [ ] Permission flags minimal and correct - [ ] Fuzz tests pass for edge cases - [ ] Invariant tests for delta accounting - [ ] Both swap directions tested - [ ] Fee calculations match Uniswap's formula