Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: v4-security-foundations description: Security-first Uniswap v4 hook development. Use when user mentions "v4 hooks", "hook security", "PoolManager", "beforeSwap", "afterSwap", or asks about V4 hook best practices, vulnerabilities, or audit requirements. allowed-tools: Read, Glob, Grep, WebFetch, Task(subagent_type:Explore) model: opus license: MIT metadata: author: uniswap version: '1.1.0'
v4 Hook Security Foundations
Security-first guide for building Uniswap v4 hooks. Hook vulnerabilities can drain user funds—understand these concepts before writing any hook code.
Threat Model
Before writing code, understand the v4 security context:
| Threat Area | Description | Mitigation | |
|---|---|---|---|
| Caller Verification | Only PoolManager should invoke hook functions | Verify msg.sender == address(poolManager) | |
| Sender Identity | msg.sender always equals PoolManager, never the end user | Use sender parameter for user identity | |
| Router Context | The sender parameter identifies the router, not the user | Implement router allowlisting | |
| State Exposure | Hook state is readable during mid-transaction execution | Avoid storing sensitive data on-chain | |
| Reentrancy Surface | External calls from hooks can enable reentrancy | Use reentrancy guards; minimize external calls |
Permission Flags Risk Matrix
All 14 hook permissions with associated risk levels:
| Permission Flag | Risk Level | Description | Security Notes | |
|---|---|---|---|---|
beforeInitialize | LOW | Called before pool creation | Validate pool parameters | |
afterInitialize | LOW | Called after pool creation | Safe for state initialization | |
beforeAddLiquidity | MEDIUM | Before LP deposits | Can block legitimate LPs | |
afterAddLiquidity | LOW | After LP deposits | Safe for tracking/rewards | |
beforeRemoveLiquidity | HIGH | Before LP withdrawals | Can trap user funds | |
afterRemoveLiquidity | LOW | After LP withdrawals | Safe for tracking | |
beforeSwap | HIGH | Before swap execution | Can manipulate prices | |
afterSwap | MEDIUM | After swap execution | Can observe final state | |
beforeDonate | LOW | Before donations | Access control only | |
afterDonate | LOW | After donations | Safe for tracking | |
beforeSwapReturnDelta | CRITICAL | Returns custom swap amounts | NoOp attack vector | |
afterSwapReturnDelta | HIGH | Modifies post-swap amounts | Can extract value | |
afterAddLiquidityReturnDelta | HIGH | Modifies LP token amounts | Can shortchange LPs | |
afterRemoveLiquidityReturnDelta | HIGH | Modifies withdrawal amounts | Can steal funds |
Risk Thresholds
- LOW: Unlikely to cause fund loss
- MEDIUM: Requires careful implementation
- HIGH: Can cause fund loss if misimplemented
- CRITICAL: Can enable complete fund theft
CRITICAL: NoOp Rug Pull Attack
The BEFORE_SWAP_RETURNS_DELTA permission (bit 10) is the most dangerous hook permission. A malicious hook can:
- Return a delta claiming it handled the entire swap
- PoolManager accepts this and settles the trade
- Hook keeps all input tokens without providing output
- User loses entire swap amount
Attack Pattern
// MALICIOUS - DO NOT USEfunction beforeSwap(address,PoolKey calldata,IPoolManager.SwapParams calldata params,bytes calldata) external override returns (bytes4, BeforeSwapDelta, uint24) {// Claim to handle the swap but steal tokensint128 amountSpecified = int128(params.amountSpecified);BeforeSwapDelta delta = toBeforeSwapDelta(amountSpecified, 0);return (BaseHook.beforeSwap.selector, delta, 0);}
Detection
Before interacting with ANY hook that has beforeSwapReturnDelta: true:
- Audit the hook code - Verify legitimate use case
- Check ownership - Is it upgradeable? By whom?
- Verify track record - Has it been audited by reputable firms?
- Start small - Test with minimal amounts first
Legitimate Uses
NoOp patterns are valid for:
- Just-in-time liquidity (JIT)
- Custom AMM curves
- Intent-based trading systems
- RFQ/PMM integrations
But each requires careful implementation and audit.
Delta Accounting Fundamentals
v4 uses a credit/debit system through the PoolManager:
Core Invariant
For every transaction: sum(deltas) == 0
The PoolManager tracks what each address owes or is owed. At transaction end, all debts must be settled.
Key Functions
| Function | Purpose | Direction | |
|---|---|---|---|
take(currency, to, amount) | Withdraw tokens from PoolManager | You receive tokens | |
settle(currency) | Pay tokens to PoolManager | You send tokens | |
sync(currency) | Update PoolManager balance tracking | Preparation for settle |
Settlement Pattern
// Correct pattern: sync before settlepoolManager.sync(currency);currency.transfer(address(poolManager), amount);poolManager.settle(currency);
Common Mistakes
- Forgetting sync: Settlement fails without sync
- Wrong order: Must sync → transfer → settle
- Partial settlement: Leaves transaction in invalid state
- Double settlement: Causes accounting errors
Access Control Patterns
PoolManager Verification
Every hook callback MUST verify the caller:
modifier onlyPoolManager() {require(msg.sender == address(poolManager), "Not PoolManager");_;}function beforeSwap(address sender,PoolKey calldata key,IPoolManager.SwapParams calldata params,bytes calldata hookData) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {// Safe to proceed}
Why This Matters
Without this check:
- Anyone can call hook functions directly
- Attackers can manipulate hook state
- Funds can be drained through fake callbacks
Router Verification Patterns
The sender parameter is the router, not the end user. For hooks that need user identity:
Allowlisting Pattern
mapping(address => bool) public allowedRouters;function beforeSwap(address sender, // This is the routerPoolKey calldata key,IPoolManager.SwapParams calldata params,bytes calldata hookData) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {require(allowedRouters[sender], "Router not allowed");// Proceed with swap}
User Identity via hookData
function beforeSwap(address sender,PoolKey calldata key,IPoolManager.SwapParams calldata params,bytes calldata hookData) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {// Decode user address from hookData (router must include it)address user = abi.decode(hookData, (address));// CAUTION: Router must be trusted to provide accurate user}
msg.sender Trap
// WRONG - msg.sender is always PoolManager in hooksfunction beforeSwap(...) external {require(msg.sender == someUser); // Always fails or wrong}// CORRECT - Use sender parameterfunction beforeSwap(address sender, ...) external {require(allowedRouters[sender], "Invalid router");}
Token Handling Hazards
Not all tokens behave like standard ERC-20s:
| Token Type | Hazard | Mitigation | |
|---|---|---|---|
| Fee-on-transfer | Received amount < sent amount | Measure actual balance changes | |
| Rebasing | Balance changes without transfers | Avoid storing raw balances | |
| ERC-777 | Transfer callbacks enable reentrancy | Use reentrancy guards | |
| Pausable | Transfers can be blocked | Handle transfer failures gracefully | |
| Blocklist | Specific addresses blocked | Test with production addresses | |
| Low decimals | Precision loss in calculations | Use appropriate scaling |
Safe Balance Check Pattern
function safeTransferIn(IERC20 token,address from,uint256 amount) internal returns (uint256 received) {uint256 balanceBefore = token.balanceOf(address(this));token.safeTransferFrom(from, address(this), amount);received = token.balanceOf(address(this)) - balanceBefore;}
Base Hook Template
Start with all permissions disabled. Enable only what you need:
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";import {Hooks} from "v4-core/src/libraries/Hooks.sol";import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";import {PoolKey} from "v4-core/src/types/PoolKey.sol";import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";contract SecureHook is BaseHook {constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}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, // Enable only if neededafterSwap: false, // Enable only if neededbeforeDonate: false,afterDonate: false,beforeSwapReturnDelta: false, // DANGER: NoOp attack vectorafterSwapReturnDelta: false, // DANGER: Can extract valueafterAddLiquidityReturnDelta: false,afterRemoveLiquidityReturnDelta: false});}// Implement only the callbacks you enabled above}
See references/base-hook-template.md for a complete implementation template.
Security Checklist
Before deploying any hook:
| # | Check | Status | |
|---|---|---|---|
| 1 | All hook callbacks verify msg.sender == poolManager | [ ] | |
| 2 | Router allowlisting implemented if needed | [ ] | |
| 3 | No unbounded loops that can cause OOG | [ ] | |
| 4 | Reentrancy guards on external calls | [ ] | |
| 5 | Delta accounting sums to zero | [ ] | |
| 6 | Fee-on-transfer tokens handled | [ ] | |
| 7 | No hardcoded addresses | [ ] | |
| 8 | Slippage parameters respected | [ ] | |
| 9 | No sensitive data stored on-chain | [ ] | |
| 10 | Upgrade mechanisms secured (if applicable) | [ ] | |
| 11 | beforeSwapReturnDelta justified if enabled | [ ] | |
| 12 | Fuzz testing completed | [ ] | |
| 13 | Invariant testing completed | [ ] |
Gas Budget Guidelines
Hook callbacks execute inside the PoolManager's transaction context. Excessive gas consumption can make swaps revert or become economically unviable.
Gas Budgets by Callback
| Callback | Target Budget | Hard Ceiling | Notes | |
|---|---|---|---|---|
beforeSwap | < 50,000 gas | 150,000 gas | Runs on every swap; keep lean | |
afterSwap | < 30,000 gas | 100,000 gas | Analytics/tracking only | |
beforeAddLiquidity | < 50,000 gas | 200,000 gas | May include access control | |
afterAddLiquidity | < 30,000 gas | 100,000 gas | Reward tracking | |
beforeRemoveLiquidity | < 50,000 gas | 200,000 gas | Lock validation | |
afterRemoveLiquidity | < 30,000 gas | 100,000 gas | Tracking/accounting | |
| Callbacks with external calls | < 100,000 gas | 300,000 gas | External DEX routing, oracles |
Common Gas Pitfalls
- Unbounded loops: Iterating over dynamic arrays (e.g., all active positions) can exceed block gas limits. Cap array sizes or use pagination.
- SSTORE in hot paths: Each new storage slot costs ~20,000 gas. Prefer transient storage (
tstore/tload) for data that doesn't persist beyond the transaction. Requires Solidity >= 0.8.24 with EVM target set tocancunor later. - External calls: Each cross-contract call adds ~2,600 gas base cost plus the callee's execution. Batch calls where possible.
- String operations: Avoid
stringmanipulation in callbacks; usebytes32for identifiers. - Redundant reads: Cache
poolManagercalls — repeatedgetSlot0()orgetLiquidity()reads cost gas each time.
Measuring Gas
# Profile a specific hook callback with Foundryforge test --match-test test_beforeSwapGas --gas-report# Snapshot gas usage across all testsforge snapshot --match-contract MyHookTest
Risk Scoring System
Calculate your hook's risk score (0-33):
| Category | Points | Criteria | |
|---|---|---|---|
| Permissions | 0-14 | Sum of enabled permission risk levels | |
| External Calls | 0-5 | Number and type of external interactions | |
| State Complexity | 0-5 | Amount of mutable state | |
| Upgrade Mechanism | 0-5 | Proxy, admin functions, etc. | |
| Token Handling | 0-4 | Non-standard token support |
Audit Tier Recommendations
| Score | Risk Level | Recommendation | |
|---|---|---|---|
| 0-5 | Low | Self-audit + peer review | |
| 6-12 | Medium | Professional audit recommended | |
| 13-20 | High | Professional audit required | |
| 21-33 | Critical | Multiple audits required |
Absolute Prohibitions
Never do these things in a hook:
- Never trust `msg.sender` for user identity - It's always PoolManager
- Never enable `beforeSwapReturnDelta` without understanding NoOp attacks
- Never store passwords, keys, or PII on-chain
- Never use `transfer()` for ETH - Use
call{value:}("") - Never assume token decimals - Always query the token
- Never use `block.timestamp` for randomness
- Never hardcode gas limits in calls
- Never ignore return values from external calls
- Never use `tx.origin` for authorization - It's a phishing vector; malicious contracts can relay calls with the original user's
tx.origin
Pre-Deployment Audit Checklist
| # | Item | Required For | |
|---|---|---|---|
| 1 | Code review by security-focused developer | All hooks | |
| 2 | Unit tests for all callbacks | All hooks | |
| 3 | Fuzz testing with Foundry | All hooks | |
| 4 | Invariant testing | Hooks with delta returns | |
| 5 | Fork testing on mainnet | All hooks | |
| 6 | Gas profiling | All hooks | |
| 7 | Formal verification | Critical hooks | |
| 8 | Slither/Mythril analysis | All hooks | |
| 9 | External audit | Medium+ risk hooks | |
| 10 | Bug bounty program | High+ risk hooks | |
| 11 | Monitoring/alerting setup | All production hooks |
See references/audit-checklist.md for detailed audit requirements.
Production Hook References
Learn from audited, production hooks:
| Project | Description | Notable Security Features | |
|---|---|---|---|
| Flaunch | Token launch platform | Multi-sig admin, timelocks | |
| EulerSwap | Lending integration | Isolated risk per market | |
| Zaha TWAMM | Time-weighted AMM | Gradual execution reduces MEV | |
| Bunni | LP management | Concentrated liquidity guards |
External Resources
Official Documentation
Security Resources
Community
- v4-hooks-skill by @igoryuzo - Community skill that inspired this guide
- v4hooks.dev - Community hook resources
Additional References
- Base Hook Template - Complete implementation starter
- Vulnerabilities Catalog - Common patterns and mitigations
- Audit Checklist - Detailed pre-deployment checklist