
V4 Security Foundations
- 866 installs
- 222 repo stars
- Updated August 4, 2026
- uniswap/uniswap-ai
v4-security-foundations is a smart contract security skill that audits a Uniswap v4 hook contract against PoolManager access control, delta accounting, and settlement invariants for developers who deploy hooks to mainnet
About
v4-security-foundations is a skill from uniswap/uniswap-ai providing a comprehensive pre-deployment audit checklist for Uniswap v4 hooks. It verifies every hook callback checks msg.sender equals address(poolManager), enforces router allowlisting with admin-protected modifications, requires two-step admin transfers, and validates delta accounting and settlement invariants before mainnet deployment. Developers reach for v4-security-foundations when reviewing custom v4 hook Solidity before launch, ensuring no code path bypasses PoolManager verification and zero addresses cannot enter allowlists. The checklist spans access control, router authorization, admin function safeguards, and accounting correctness for production DeFi deployments.
- Access-control section covers PoolManager-only callbacks, router allowlists, and admin transfer safeguards
- Delta-accounting section enforces zero-sum deltas, fee-on-transfer/rebasing handling, and sync-transfer-settle ordering
- Token-handling checks call out ERC-777 reentrancy and decimal queries instead of hard-coded assumptions
- Settlement-flow checks guard against partial settlements and bad state after reverts
- Permission section continues the audit beyond accounting (checklist extends past delta invariants)
V4 Security Foundations by the numbers
- 866 all-time installs (skills.sh)
- +38 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #424 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/uniswap/uniswap-ai --skill v4-security-foundationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 866 |
|---|---|
| repo stars | ★ 222 |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | uniswap/uniswap-ai ↗ |
How do you audit Uniswap v4 hooks before deploy?
Audit a Uniswap v4 hook contract against PoolManager access control, delta accounting, and settlement invariants before you deploy to mainnet.
Who is it for?
Solidity developers deploying custom Uniswap v4 hooks who need a structured security checklist before mainnet launch.
Skip if: Non-Uniswap DeFi contracts or teams still on Uniswap v2/v3 without v4 PoolManager hook integrations.
When should I use this skill?
A developer prepares to deploy a Uniswap v4 hook contract and needs PoolManager, delta, and settlement invariant verification.
What you get
Completed pre-deployment audit checklist, verified PoolManager access controls, and validated delta accounting invariants.
- Completed audit checklist
- Access control findings
Files
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:
1. Return a delta claiming it handled the entire swap 2. PoolManager accepts this and settles the trade 3. Hook keeps all input tokens without providing output 4. User loses entire swap amount
Attack Pattern
// MALICIOUS - DO NOT USE
function beforeSwap(
address,
PoolKey calldata,
IPoolManager.SwapParams calldata params,
bytes calldata
) external override returns (bytes4, BeforeSwapDelta, uint24) {
// Claim to handle the swap but steal tokens
int128 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:
1. Audit the hook code - Verify legitimate use case 2. Check ownership - Is it upgradeable? By whom? 3. Verify track record - Has it been audited by reputable firms? 4. 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) == 0The 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 settle
poolManager.sync(currency);
currency.transfer(address(poolManager), amount);
poolManager.settle(currency);Common Mistakes
1. Forgetting sync: Settlement fails without sync 2. Wrong order: Must sync → transfer → settle 3. Partial settlement: Leaves transaction in invalid state 4. 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 router
PoolKey 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 hooks
function beforeSwap(...) external {
require(msg.sender == someUser); // Always fails or wrong
}
// CORRECT - Use sender parameter
function 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: MIT
pragma 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 needed
afterSwap: false, // Enable only if needed
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false, // DANGER: NoOp attack vector
afterSwapReturnDelta: false, // DANGER: Can extract value
afterAddLiquidityReturnDelta: 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
1. Unbounded loops: Iterating over dynamic arrays (e.g., all active positions) can exceed block gas limits. Cap array sizes or use pagination. 2. 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 to cancun or later. 3. External calls: Each cross-contract call adds ~2,600 gas base cost plus the callee's execution. Batch calls where possible. 4. String operations: Avoid string manipulation in callbacks; use bytes32 for identifiers. 5. Redundant reads: Cache poolManager calls — repeated getSlot0() or getLiquidity() reads cost gas each time.
Measuring Gas
# Profile a specific hook callback with Foundry
forge test --match-test test_beforeSwapGas --gas-report
# Snapshot gas usage across all tests
forge 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:
1. Never trust `msg.sender` for user identity - It's always PoolManager 2. Never enable `beforeSwapReturnDelta` without understanding NoOp attacks 3. Never store passwords, keys, or PII on-chain 4. Never use `transfer()` for ETH - Use call{value:}("") 5. Never assume token decimals - Always query the token 6. Never use `block.timestamp` for randomness 7. Never hardcode gas limits in calls 8. Never ignore return values from external calls 9. 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
v4 Hook Pre-Deployment Audit Checklist
Comprehensive checklist for auditing Uniswap v4 hooks before deployment.
1. Access Control
1.1 PoolManager Verification
- [ ] All hook callbacks verify
msg.sender == address(poolManager) - [ ] Verification uses modifier or explicit check at function start
- [ ] No code paths bypass the verification
1.2 Router Authorization
- [ ] Router allowlisting implemented if hook restricts callers
- [ ] Allowlist modifications are admin-protected
- [ ] Cannot add zero address to allowlist
1.3 Admin Functions
- [ ] Admin role transfer is two-step (propose/accept) or timelock-protected
- [ ] Critical admin functions have event emissions
- [ ] Admin cannot brick the contract (e.g., renounce without safeguards)
2. Delta Accounting
2.1 Balance Invariants
- [ ] All returned deltas are backed by actual token movements
- [ ]
take()calls match returned delta values - [ ]
settle()calls properly account for received tokens - [ ] Invariant:
sum(all deltas) == 0for every transaction
2.2 Token Handling
- [ ] Fee-on-transfer tokens handled (measure actual received amounts)
- [ ] Rebasing tokens handled or explicitly blocked
- [ ] ERC-777 reentrancy considered
- [ ] Token decimals not assumed (queried from token)
2.3 Settlement Flow
- [ ] Correct order:
sync()->transfer()->settle() - [ ] No partial settlements left hanging
- [ ] Error handling doesn't leave accounting in bad state
3. Permissions Review
3.1 Enabled Permissions Audit
For each enabled permission, document:
| Permission | Enabled | Justification | Risk Level |
|---|---|---|---|
| beforeInitialize | [ ] | ||
| afterInitialize | [ ] | ||
| beforeAddLiquidity | [ ] | ||
| afterAddLiquidity | [ ] | ||
| beforeRemoveLiquidity | [ ] | ||
| afterRemoveLiquidity | [ ] | ||
| beforeSwap | [ ] | ||
| afterSwap | [ ] | ||
| beforeDonate | [ ] | ||
| afterDonate | [ ] | ||
| beforeSwapReturnDelta | [ ] | ||
| afterSwapReturnDelta | [ ] | ||
| afterAddLiquidityReturnDelta | [ ] | ||
| afterRemoveLiquidityReturnDelta | [ ] |
3.2 Delta Return Permissions (Critical Review)
If any delta return permission is enabled:
- [ ] NoOp attack vector analyzed and mitigated
- [ ] All code paths returning non-zero deltas reviewed
- [ ] Deltas are always backed by liquidity provision
- [ ] Multiple independent reviewers have verified
4. Reentrancy Protection
4.1 External Calls
- [ ] All external calls identified and documented
- [ ] Reentrancy guards applied where needed
- [ ] State changes follow checks-effects-interactions pattern
- [ ] No callbacks to untrusted contracts
4.2 Token Callbacks
- [ ] ERC-777
tokensReceivedhook considered - [ ] ERC-721/1155 callbacks considered if applicable
- [ ] Flash loan callbacks considered
5. Gas and DoS
5.1 Loop Bounds
- [ ] All loops have maximum iteration limits
- [ ] Limits are appropriate for block gas limit
- [ ] User-controllable arrays bounded
5.2 Gas Estimation
- [ ] Gas usage tested for worst-case scenarios
- [ ] No operations that could exceed block gas limit
- [ ] External call gas limits are reasonable
5.3 DoS Vectors
- [ ] No unbounded array iterations
- [ ] No unbounded mapping iterations
- [ ] Failed external calls don't block hook functionality
- [ ] Griefing attacks considered
6. Input Validation
6.1 Parameter Validation
- [ ] All external inputs validated
- [ ] hookData properly decoded and validated
- [ ] No assumptions about parameter ranges
- [ ] Overflow/underflow protection (Solidity 0.8.x or SafeMath)
6.2 Pool Key Validation
- [ ] Hook is authorized for the pool
- [ ] Currency addresses validated
- [ ] Fee tier validated if relevant
7. State Management
7.1 Storage
- [ ] No sensitive data stored on-chain
- [ ] Storage slots don't collide (if using assembly)
- [ ] Transient storage used appropriately
- [ ] Storage variables initialized correctly
7.2 State Transitions
- [ ] All state transitions have valid preconditions
- [ ] No invalid intermediate states possible
- [ ] State can be recovered from errors
8. Upgrade Safety (if applicable)
8.1 Proxy Patterns
- [ ] Upgrade mechanism is access-controlled
- [ ] Timelock or governance required for upgrades
- [ ] Storage layout documented
- [ ] Upgrade path tested
8.2 Migration Safety
- [ ] Old state can be migrated to new implementation
- [ ] No loss of funds during migration
- [ ] Rollback plan exists
9. Testing Requirements
9.1 Unit Tests
- [ ] Every public/external function tested
- [ ] Every require/revert condition tested
- [ ] Edge cases tested (zero amounts, max values, etc.)
- [ ] All code paths covered
9.2 Fuzz Testing
- [ ] Foundry fuzz tests for all state-changing functions
- [ ] Minimum 10,000 fuzz runs
- [ ] Custom fuzz invariants defined
- [ ] No failures in extended fuzzing
9.3 Invariant Testing
- [ ] Core invariants defined and tested
- [ ] Delta accounting invariants tested
- [ ] Balance invariants tested
- [ ] Access control invariants tested
9.4 Integration Testing
- [ ] Fork tests against mainnet state
- [ ] Tests with real pool addresses
- [ ] Tests with various token types
- [ ] End-to-end swap flow tested
10. Static Analysis
10.1 Automated Tools
- [ ] Slither analysis completed (all findings addressed)
- [ ] Mythril analysis completed
- [ ] Solhint linting passed
- [ ] Custom detectors for v4-specific issues
10.2 Manual Review
- [ ] Line-by-line code review completed
- [ ] Business logic reviewed against specification
- [ ] Comparison with similar audited hooks
11. Documentation
11.1 Code Documentation
- [ ] NatSpec comments for all public functions
- [ ] Complex logic explained in comments
- [ ] Assumptions documented
11.2 External Documentation
- [ ] Architecture diagram
- [ ] Threat model document
- [ ] Deployment procedure
- [ ] Emergency procedures
12. Deployment Preparation
12.1 Pre-Deployment
- [ ] Hook address mined with correct permission bits
- [ ] Constructor parameters verified
- [ ] Deployment script reviewed and tested
- [ ] Gas estimation for deployment
12.2 Post-Deployment
- [ ] Verify source code on block explorer
- [ ] Verify permissions match expected
- [ ] Test transaction on mainnet
- [ ] Monitoring and alerting configured
Audit Sign-Off
| Role | Name | Date | Signature |
|---|---|---|---|
| Lead Auditor | |||
| Security Reviewer | |||
| Code Owner |
Risk Assessment Summary
| Category | Risk Level | Notes |
|---|---|---|
| Access Control | ||
| Delta Accounting | ||
| Reentrancy | ||
| DoS | ||
| Upgrade Safety | ||
| Overall |
Findings Log
| ID | Severity | Description | Status | Resolution |
|---|---|---|---|---|
---
Audit Tier Guidelines
Based on risk assessment, determine required audit level:
Tier 1: Self-Audit (Risk Score 0-5)
- Internal code review
- Full test coverage
- Static analysis tools
Tier 2: Peer Audit (Risk Score 6-12)
- Tier 1 requirements
- External developer review
- Extended fuzz testing
Tier 3: Professional Audit (Risk Score 13-20)
- Tier 2 requirements
- Audit by security firm
- Bug bounty program
Tier 4: Multi-Audit (Risk Score 21+)
- Tier 3 requirements
- Multiple independent audits
- Formal verification considered
- Extended bug bounty with significant rewards
Base Hook Template
A security-first Solidity template for Uniswap v4 hooks with all permissions disabled by default.
Complete Template
// SPDX-License-Identifier: MIT
pragma 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 {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
/// @title SecureHook
/// @notice Security-first v4 hook template
/// @dev All permissions disabled by default - enable only what you need
contract SecureHook is BaseHook {
using PoolIdLibrary for PoolKey;
// ═══════════════════════════════════════════════════════════════════════
// ERRORS
// ═══════════════════════════════════════════════════════════════════════
error NotPoolManager();
error RouterNotAllowed();
error ZeroAddress();
error NotAdmin();
error Unauthorized();
error AdminTransferToSelf();
error NoPendingAdmin();
// ═══════════════════════════════════════════════════════════════════════
// EVENTS
// ═══════════════════════════════════════════════════════════════════════
event RouterAdded(address indexed router);
event RouterRemoved(address indexed router);
event AdminTransferProposed(address indexed currentAdmin, address indexed proposedAdmin);
event AdminTransferred(address indexed previousAdmin, address indexed newAdmin);
// ═══════════════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════════════
/// @notice Allowlisted routers that can interact with this hook
mapping(address => bool) public allowedRouters;
/// @notice Hook administrator
address public admin;
/// @notice Pending administrator for two-step transfer
/// @dev Set by proposeAdmin(), cleared by acceptAdmin(). Only the pendingAdmin
/// address can call acceptAdmin() to complete the transfer. This ensures
/// admin privileges are never transferred to an address that cannot
/// interact with the contract (e.g., a typo or non-existent wallet).
address public pendingAdmin;
// ═══════════════════════════════════════════════════════════════════════
// MODIFIERS
// ═══════════════════════════════════════════════════════════════════════
/// @notice Ensures caller is the PoolManager
modifier onlyPoolManager() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
/// @notice Ensures sender (router) is allowlisted
modifier onlyAllowedRouter(address sender) {
if (!allowedRouters[sender]) revert RouterNotAllowed();
_;
}
/// @notice Ensures caller is admin
modifier onlyAdmin() {
if (msg.sender != admin) revert NotAdmin();
_;
}
// ═══════════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════════
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {
admin = msg.sender;
}
// ═══════════════════════════════════════════════════════════════════════
// HOOK PERMISSIONS - ALL DISABLED BY DEFAULT
// ═══════════════════════════════════════════════════════════════════════
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: false,
beforeDonate: false,
afterDonate: false,
// DANGER ZONE - These enable delta manipulation
beforeSwapReturnDelta: false, // CRITICAL: NoOp attack vector
afterSwapReturnDelta: false, // HIGH: Can extract value
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// ═══════════════════════════════════════════════════════════════════════
// HOOK CALLBACKS - Implement only what you enable
// ═══════════════════════════════════════════════════════════════════════
// Uncomment and implement only the callbacks you need
/*
function beforeInitialize(
address sender,
PoolKey calldata key,
uint160 sqrtPriceX96
) external override onlyPoolManager returns (bytes4) {
// Validate pool parameters here
return BaseHook.beforeInitialize.selector;
}
function afterInitialize(
address sender,
PoolKey calldata key,
uint160 sqrtPriceX96,
int24 tick
) external override onlyPoolManager returns (bytes4) {
// Initialize hook state here
return BaseHook.afterInitialize.selector;
}
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager onlyAllowedRouter(sender) returns (bytes4, BeforeSwapDelta, uint24) {
// Pre-swap logic here
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
function afterSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
BalanceDelta delta,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, int128) {
// Post-swap logic here
return (BaseHook.afterSwap.selector, 0);
}
*/
// ═══════════════════════════════════════════════════════════════════════
// ADMIN FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════
/// @notice Add a router to the allowlist
/// @param router The router address to allow
function addAllowedRouter(address router) external onlyAdmin {
if (router == address(0)) revert ZeroAddress();
allowedRouters[router] = true;
emit RouterAdded(router);
}
/// @notice Remove a router from the allowlist
/// @param router The router address to remove
function removeAllowedRouter(address router) external onlyAdmin {
allowedRouters[router] = false;
emit RouterRemoved(router);
}
/// @notice Propose a new admin (two-step transfer for safety)
/// @dev Two-step transfer prevents accidental loss of admin privileges.
/// Step 1: Current admin proposes new admin via proposeAdmin()
/// Step 2: Proposed admin accepts the role via acceptAdmin()
/// Calling proposeAdmin() again overwrites any existing pending transfer.
/// Only the most recent proposed admin can call acceptAdmin().
/// @param newAdmin The proposed new admin address (must not be zero or current admin)
function proposeAdmin(address newAdmin) external onlyAdmin {
if (newAdmin == address(0)) revert ZeroAddress();
if (newAdmin == admin) revert AdminTransferToSelf();
pendingAdmin = newAdmin;
emit AdminTransferProposed(admin, newAdmin);
}
/// @notice Accept the admin role (must be called by the pending admin)
/// @dev Completes the two-step admin transfer. The caller must be the address
/// previously set via proposeAdmin(). After acceptance, pendingAdmin is
/// cleared to address(0) to prevent replay. Reverts if no transfer is
/// pending (pendingAdmin == address(0)) to avoid silent no-ops.
function acceptAdmin() external {
if (pendingAdmin == address(0)) revert NoPendingAdmin();
if (msg.sender != pendingAdmin) revert Unauthorized();
address previousAdmin = admin;
admin = pendingAdmin;
pendingAdmin = address(0);
emit AdminTransferred(previousAdmin, admin);
}
}Usage Guide
1. Copy the template
Copy this template to your project and rename appropriately.
2. Enable only needed permissions
In getHookPermissions(), set true only for callbacks you implement:
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
// ... other permissions false ...
beforeSwap: true, // Enable this
afterSwap: true, // And this
// ... rest false ...
});
}3. Implement enabled callbacks
Uncomment and implement only the callbacks you enabled:
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager onlyAllowedRouter(sender) returns (bytes4, BeforeSwapDelta, uint24) {
// Your logic here
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}4. Deploy with correct address
v4 hooks require specific address patterns. Use the hook miner:
forge script script/DeployHook.s.sol --rpc-url $RPC_URLSecurity Checklist for This Template
- [x] PoolManager verification via
onlyPoolManagermodifier - [x] Router allowlisting via
onlyAllowedRoutermodifier - [x] All dangerous permissions disabled by default
- [x] Admin functions protected
- [x] Zero address checks
- [x] Two-step admin transfer prevents accidental privilege loss
- [ ] Add reentrancy guard if making external calls
- [ ] Add your specific business logic tests
v4 Hook Vulnerabilities Catalog
Common vulnerability patterns in Uniswap v4 hooks with detection methods and mitigations.
Critical Vulnerabilities
1. NoOp Rug Pull (CRITICAL)
Description: Hook with beforeSwapReturnDelta enabled returns a delta claiming to handle the swap but steals input tokens.
Vulnerable Pattern:
// VULNERABLE - Do not use
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
// Claims to handle swap but provides nothing
BeforeSwapDelta delta = toBeforeSwapDelta(params.amountSpecified, 0);
return (BaseHook.beforeSwap.selector, delta, 0);
}Detection:
- Check if
beforeSwapReturnDelta: truein permissions - Verify hook actually provides liquidity for claimed delta
- Audit all code paths that return non-zero deltas
Mitigation:
- Don't enable
beforeSwapReturnDeltaunless absolutely necessary - If enabled, ensure delta is backed by actual liquidity provision
- Require multiple audits for hooks with this permission
2. Missing PoolManager Verification (CRITICAL)
Description: Hook callbacks don't verify caller is the PoolManager, allowing direct manipulation.
Vulnerable Pattern:
// VULNERABLE - No caller check
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external returns (bytes4, BeforeSwapDelta, uint24) {
// Anyone can call this directly!
_updateState(params);
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}Detection:
- Search for hook callbacks without
onlyPoolManageror equivalent - Check first line of each callback for msg.sender verification
Mitigation:
modifier onlyPoolManager() {
require(msg.sender == address(poolManager), "Not PoolManager");
_;
}
function beforeSwap(...) external onlyPoolManager returns (...) {
// Safe
}3. Delta Accounting Mismatch (CRITICAL)
Description: Hook returns deltas that don't balance, causing transaction revert or fund loss.
Vulnerable Pattern:
// VULNERABLE - Deltas don't balance
function afterSwap(...) external returns (bytes4, int128) {
// Takes tokens but doesn't account for them
poolManager.take(currency, address(this), amount);
return (BaseHook.afterSwap.selector, 0); // Wrong delta!
}Detection:
- Trace all
take(),settle(), and delta returns - Verify sum equals zero for all code paths
- Fuzz test with random amounts
Mitigation:
function afterSwap(...) external returns (bytes4, int128) {
uint256 amount = calculateAmount();
// Bounds check: ensure amount fits in int128 to prevent overflow
require(amount <= uint256(type(int128).max), "Amount exceeds int128 max");
poolManager.take(currency, address(this), amount);
// Cast sequence: uint256 → uint128 → int128
// The uint128 intermediate prevents treating large values as negative,
// since direct uint256 → int128 would misinterpret values > int128.max
return (BaseHook.afterSwap.selector, int128(uint128(amount)));
}High Severity Vulnerabilities
4. Reentrancy via External Calls (HIGH)
Description: Hook makes external call that reenters PoolManager before state is finalized.
Vulnerable Pattern:
// VULNERABLE - Reentrancy possible
function afterSwap(...) external returns (bytes4, int128) {
state = newState;
externalContract.callback(); // Can reenter!
return (BaseHook.afterSwap.selector, 0);
}Detection:
- Identify all external calls in hook callbacks
- Check if state changes happen before external calls
- Look for ERC-777 tokens or contracts with callbacks
Mitigation:
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureHook is BaseHook, ReentrancyGuard {
function afterSwap(...) external nonReentrant returns (bytes4, int128) {
// BEST PRACTICE: Follow CEI pattern - state changes BEFORE external calls
// The nonReentrant modifier is a safety net, not a replacement for CEI
state = newState;
externalContract.callback();
return (BaseHook.afterSwap.selector, 0);
}
}5. Unbounded Loop DoS (HIGH)
Description: Hook iterates over unbounded array, causing out-of-gas.
Vulnerable Pattern:
// VULNERABLE - Unbounded loop
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
for (uint i = 0; i < participants.length; i++) { // Can grow forever
_processParticipant(participants[i]);
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}Detection:
- Find all loops in hook callbacks
- Check if loop bounds are user-controllable
- Test with large arrays
Mitigation:
uint256 constant MAX_PARTICIPANTS = 100;
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
uint256 len = participants.length > MAX_PARTICIPANTS ? MAX_PARTICIPANTS : participants.length;
for (uint i = 0; i < len; i++) {
_processParticipant(participants[i]);
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}6. Liquidity Lock (HIGH)
Description: beforeRemoveLiquidity can permanently trap LP funds.
Vulnerable Pattern:
// VULNERABLE - Can lock funds forever
function beforeRemoveLiquidity(...) external returns (bytes4) {
require(block.timestamp > unlockTime, "Locked"); // What if unlockTime is set to max?
return BaseHook.beforeRemoveLiquidity.selector;
}Detection:
- Check all conditions in
beforeRemoveLiquidity - Verify unlock conditions are achievable
- Look for admin-controlled lock parameters
Mitigation:
uint256 constant MAX_LOCK_DURATION = 365 days;
function setLockDuration(uint256 duration) external onlyAdmin {
require(duration <= MAX_LOCK_DURATION, "Too long");
lockDuration = duration;
}Medium Severity Vulnerabilities
7. Price Manipulation via Single Block (MEDIUM)
Description: Hook uses single-block price for decisions, enabling manipulation.
Vulnerable Pattern:
// VULNERABLE - Single block price
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
uint256 currentPrice = oracle.latestPrice(); // Flashloan manipulable
if (currentPrice < threshold) {
revert("Price too low");
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}Detection:
- Find price/rate fetching in hooks
- Check if TWAP or multiple sources used
- Test with flash loan scenarios
Mitigation:
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
uint256 twapPrice = oracle.getTWAP(30 minutes); // Use TWAP
if (twapPrice < threshold) {
revert("Price too low");
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}8. Missing Slippage Protection (MEDIUM)
Description: Hook doesn't enforce user-specified slippage limits.
Vulnerable Pattern:
// VULNERABLE - Ignores slippage
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
// Modifies amounts without checking slippage
int256 modifiedAmount = params.amountSpecified * 99 / 100;
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(modifiedAmount, 0), 0);
}Detection:
- Check if hookData contains slippage parameters
- Verify slippage is enforced when amounts modified
- Test with extreme market conditions
Mitigation:
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external returns (bytes4, BeforeSwapDelta, uint24) {
(uint256 minOutput) = abi.decode(hookData, (uint256));
// Enforce slippage in hook logic
require(calculatedOutput >= minOutput, "Slippage exceeded");
return (BaseHook.beforeSwap.selector, delta, 0);
}9. Fee-on-Transfer Token Mismatch (MEDIUM)
Description: Hook assumes transferred amount equals requested amount.
Vulnerable Pattern:
// VULNERABLE - Doesn't account for transfer fees
function _handleTransfer(IERC20 token, uint256 amount) internal {
token.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount; // Wrong if fee-on-transfer!
}Detection:
- Find all token transfers
- Check if actual received amount is verified
- Test with fee-on-transfer tokens
Mitigation:
function _handleTransfer(IERC20 token, uint256 amount) internal returns (uint256 received) {
uint256 balanceBefore = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
received = token.balanceOf(address(this)) - balanceBefore;
balances[msg.sender] += received;
}Low Severity Vulnerabilities
10. Hardcoded Addresses (LOW)
Description: Hook uses hardcoded contract addresses instead of parameters.
Issue:
// PROBLEMATIC - Hardcoded address
address constant ORACLE = 0x1234567890123456789012345678901234567890;Mitigation:
address public immutable oracle;
constructor(IPoolManager _poolManager, address _oracle) BaseHook(_poolManager) {
oracle = _oracle;
}11. Missing Event Emissions (LOW)
Description: State changes not logged, making off-chain tracking difficult.
Mitigation:
event RouterAdded(address indexed router);
event RouterRemoved(address indexed router);
function addAllowedRouter(address router) external onlyAdmin {
allowedRouters[router] = true;
emit RouterAdded(router);
}12. Unchecked Return Values (LOW)
Description: External call return values ignored.
Vulnerable Pattern:
// VULNERABLE - Ignores return value
token.transfer(recipient, amount);Mitigation:
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(recipient, amount);Vulnerability Detection Tools
Static Analysis
- Slither:
slither . --detect all - Mythril:
myth analyze contracts/Hook.sol - Solhint:
solhint 'contracts/**/*.sol'
Dynamic Analysis
- Foundry Fuzz:
forge test --fuzz-runs 10000 - Echidna: Property-based fuzzing
- Medusa: Parallel fuzzing
Manual Review Checklist
1. Trace all external calls 2. Verify all delta accounting 3. Check all access control 4. Review all state changes 5. Analyze all loops 6. Test all edge cases
Related skills
FAQ
What does v4-security-foundations audit in hook contracts?
v4-security-foundations audits Uniswap v4 hooks for PoolManager msg.sender verification, router allowlisting, admin function safeguards, and delta accounting plus settlement invariants before mainnet deployment.
When should developers run v4-security-foundations?
v4-security-foundations should run immediately before deploying custom Uniswap v4 hook Solidity to mainnet, after implementation but before irreversible on-chain launch.
Is V4 Security Foundations safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.