v4-security-foundations by uniswap/uniswap-ai
npx skills add https://github.com/uniswap/uniswap-ai --skill v4-security-foundations构建 Uniswap v4 hook 的安全优先指南。Hook 漏洞可能导致用户资金流失——在编写任何 hook 代码之前,请务必理解这些概念。
在编写代码之前,请理解 v4 的安全上下文:
| 威胁领域 | 描述 | 缓解措施 |
|---|---|---|
| 调用者验证 | 只有 PoolManager 应该调用 hook 函数 | 验证 msg.sender == address(poolManager) |
| 发送者身份 | msg.sender 始终等于 PoolManager,而非最终用户 | 使用 sender 参数来识别用户身份 |
| 路由器上下文 | 参数标识的是路由器,而非用户 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
sender| 实施路由器白名单机制 |
| 状态暴露 | Hook 状态在交易执行期间是可读的 | 避免在链上存储敏感数据 |
| 重入攻击面 | 来自 hook 的外部调用可能启用重入攻击 | 使用重入锁;最小化外部调用 |
所有 14 个 hook 权限及其相关风险等级:
| 权限标志 | 风险等级 | 描述 | 安全注意事项 |
|---|---|---|---|
beforeInitialize | 低 | 在资金池创建前调用 | 验证资金池参数 |
afterInitialize | 低 | 在资金池创建后调用 | 适用于状态初始化 |
beforeAddLiquidity | 中 | 在流动性提供者存款前调用 | 可能阻止合法的流动性提供者 |
afterAddLiquidity | 低 | 在流动性提供者存款后调用 | 适用于跟踪/奖励 |
beforeRemoveLiquidity | 高 | 在流动性提供者提款前调用 | 可能锁定用户资金 |
afterRemoveLiquidity | 低 | 在流动性提供者提款后调用 | 适用于跟踪 |
beforeSwap | 高 | 在交换执行前调用 | 可能操纵价格 |
afterSwap | 中 | 在交换执行后调用 | 可以观察最终状态 |
beforeDonate | 低 | 在捐赠前调用 | 仅用于访问控制 |
afterDonate | 低 | 在捐赠后调用 | 适用于跟踪 |
beforeSwapReturnDelta | 严重 | 返回自定义交换数量 | NoOp 攻击向量 |
afterSwapReturnDelta | 高 | 修改交换后数量 | 可能提取价值 |
afterAddLiquidityReturnDelta | 高 | 修改流动性提供者代币数量 | 可能克扣流动性提供者 |
afterRemoveLiquidityReturnDelta | 高 | 修改提款数量 | 可能窃取资金 |
BEFORE_SWAP_RETURNS_DELTA 权限(第 10 位)是最危险的 hook 权限。恶意 hook 可以:
// 恶意代码 - 请勿使用
function beforeSwap(
address,
PoolKey calldata,
IPoolManager.SwapParams calldata params,
bytes calldata
) external override returns (bytes4, BeforeSwapDelta, uint24) {
// 声称处理了交换但窃取代币
int128 amountSpecified = int128(params.amountSpecified);
BeforeSwapDelta delta = toBeforeSwapDelta(amountSpecified, 0);
return (BaseHook.beforeSwap.selector, delta, 0);
}
在与任何启用了 beforeSwapReturnDelta: true 的 hook 交互之前:
NoOp 模式在以下情况下是有效的:
但每种情况都需要仔细实现和审计。
v4 通过 PoolManager 使用信用/借记系统:
For every transaction: sum(deltas) == 0
PoolManager 跟踪每个地址欠款或被欠款。在交易结束时,所有债务必须结清。
| 函数 | 目的 | 方向 |
|---|---|---|
take(currency, to, amount) | 从 PoolManager 提取代币 | 您接收代币 |
settle(currency) | 向 PoolManager 支付代币 | 您发送代币 |
sync(currency) | 更新 PoolManager 余额跟踪 | 为结算做准备 |
// 正确模式:结算前同步
poolManager.sync(currency);
currency.transfer(address(poolManager), amount);
poolManager.settle(currency);
每个 hook 回调都必须验证调用者:
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) {
// 可以安全继续执行
}
如果没有此检查:
sender 参数是路由器,而非最终用户。对于需要用户身份的 hook:
mapping(address => bool) public allowedRouters;
function beforeSwap(
address sender, // 这是路由器
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
require(allowedRouters[sender], "Router not allowed");
// 继续执行交换
}
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
// 从 hookData 解码用户地址(路由器必须包含它)
address user = abi.decode(hookData, (address));
// 注意:必须信任路由器提供准确的用户信息
}
// 错误 - 在 hook 中,msg.sender 始终是 PoolManager
function beforeSwap(...) external {
require(msg.sender == someUser); // 总是失败或错误
}
// 正确 - 使用 sender 参数
function beforeSwap(address sender, ...) external {
require(allowedRouters[sender], "Invalid router");
}
并非所有代币都像标准 ERC-20 那样运作:
| 代币类型 | 风险 | 缓解措施 |
|---|---|---|
| 转账收费代币 | 接收金额 < 发送金额 | 测量实际余额变化 |
| 弹性供应代币 | 余额变化无需转账 | 避免存储原始余额 |
| ERC-777 | 转账回调启用重入攻击 | 使用重入锁 |
| 可暂停代币 | 转账可能被阻止 | 优雅处理转账失败 |
| 黑名单代币 | 特定地址被阻止 | 使用生产地址进行测试 |
| 低精度代币 | 计算中精度损失 | 使用适当的缩放比例 |
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;
}
从禁用所有权限开始。仅启用您需要的权限:
// 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, // 仅在需要时启用
afterSwap: false, // 仅在需要时启用
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false, // 危险:NoOp 攻击向量
afterSwapReturnDelta: false, // 危险:可能提取价值
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// 仅实现您在上面启用的回调函数
}
查看 references/base-hook-template.md 获取完整的实现模板。
在部署任何 hook 之前:
---|---|---
1 | 所有 hook 回调都验证 msg.sender == poolManager | [ ]
2 | 如果需要,已实现路由器白名单 | [ ]
3 | 没有可能导致 OOG 的无限制循环 | [ ]
4 | 外部调用上有重入锁 | [ ]
5 | Delta 记账总和为零 | [ ]
6 | 已处理转账收费代币 | [ ]
7 | 没有硬编码地址 | [ ]
8 | 滑点参数得到遵守 | [ ]
9 | 链上没有存储敏感数据 | [ ]
10 | 升级机制安全(如果适用) | [ ]
11 | 如果启用 beforeSwapReturnDelta,有正当理由 | [ ]
12 | 已完成模糊测试 | [ ]
13 | 已完成不变量测试 | [ ]
Hook 回调在 PoolManager 的交易上下文中执行。过高的 gas 消耗可能导致交换回滚或在经济上不可行。
| 回调 | 目标预算 | 硬性上限 | 备注 |
|---|---|---|---|
beforeSwap | < 50,000 gas | 150,000 gas | 每次交换都运行;保持精简 |
afterSwap | < 30,000 gas | 100,000 gas | 仅用于分析/跟踪 |
beforeAddLiquidity | < 50,000 gas | 200,000 gas | 可能包含访问控制 |
afterAddLiquidity | < 30,000 gas | 100,000 gas | 奖励跟踪 |
beforeRemoveLiquidity | < 50,000 gas | 200,000 gas | 锁验证 |
afterRemoveLiquidity | < 30,000 gas | 100,000 gas | 跟踪/记账 |
| 包含外部调用的回调 | < 100,000 gas | 300,000 gas | 外部 DEX 路由、预言机 |
cancun 或更高版本。string 操作;使用 bytes32 作为标识符。poolManager 调用——重复的 getSlot0() 或 getLiquidity() 读取每次都会消耗 gas。# 使用 Foundry 分析特定的 hook 回调
forge test --match-test test_beforeSwapGas --gas-report
# 在所有测试中快照 gas 使用情况
forge snapshot --match-contract MyHookTest
计算您的 hook 的风险评分:
| 类别 | 分数 | 标准 |
|---|---|---|
| 权限 | 0-14 | 已启用权限风险等级的总和 |
| 外部调用 | 0-5 | 外部交互的数量和类型 |
| 状态复杂性 | 0-5 | 可变状态的数量 |
| 升级机制 | 0-5 | 代理、管理函数等 |
| 代币处理 | 0-4 | 非标准代币支持 |
| 分数 | 风险等级 | 建议 |
|---|---|---|
| 0-5 | 低 | 自我审计 + 同行评审 |
| 6-12 | 中 | 建议进行专业审计 |
| 13-20 | 高 | 需要进行专业审计 |
| 21-33 | 严重 | 需要进行多次审计 |
在 hook 中永远不要做这些事情:
msg.sender 作为用户身份 - 它始终是 PoolManagerbeforeSwapReturnDeltatransfer() 发送 ETH - 使用 call{value:}("")block.timestamp 作为随机源tx.origin 进行授权 - 它是钓鱼攻击向量;恶意合约可以以原始用户的 tx.origin 中继调用---|---|---
1 | 由专注于安全的开发人员进行代码审查 | 所有 hook
2 | 所有回调的单元测试 | 所有 hook
3 | 使用 Foundry 进行模糊测试 | 所有 hook
4 | 不变量测试 | 具有 delta 返回的 hook
5 | 在主网上进行分叉测试 | 所有 hook
6 | Gas 分析 | 所有 hook
7 | 形式化验证 | 关键 hook
8 | Slither/Mythril 分析 | 所有 hook
9 | 外部审计 | 中风险及以上 hook
10 | 漏洞赏金计划 | 高风险及以上 hook
11 | 监控/警报设置 | 所有生产环境 hook
查看 references/audit-checklist.md 获取详细的审计要求。
从经过审计的生产环境 hook 中学习:
| 项目 | 描述 | 显著的安全特性 |
|---|---|---|
| Flaunch | 代币启动平台 | 多签管理、时间锁 |
| EulerSwap | 借贷集成 | 每个市场的隔离风险 |
| Zaha TWAMM | 时间加权 AMM | 逐步执行减少 MEV |
| Bunni | 流动性提供者管理 | 集中流动性保护 |
每周安装次数
288
仓库
GitHub 星标数
185
首次出现
2026年2月12日
安全审计
安装于
codex274
opencode272
gemini-cli269
github-copilot267
kimi-cli264
cursor264
Security-first guide for building Uniswap v4 hooks. Hook vulnerabilities can drain user funds—understand these concepts before writing any hook code.
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 |
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 |
The BEFORE_SWAP_RETURNS_DELTA permission (bit 10) is the most dangerous hook permission. A malicious hook can:
// 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);
}
Before interacting with ANY hook that has beforeSwapReturnDelta: true:
NoOp patterns are valid for:
But each requires careful implementation and audit.
v4 uses a credit/debit system through the PoolManager:
For every transaction: sum(deltas) == 0
The PoolManager tracks what each address owes or is owed. At transaction end, all debts must be settled.
| 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 |
// Correct pattern: sync before settle
poolManager.sync(currency);
currency.transfer(address(poolManager), amount);
poolManager.settle(currency);
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
}
Without this check:
The sender parameter is the router, not the end user. For hooks that need user identity:
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
}
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
}
// 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");
}
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 |
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;
}
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.
Before deploying any hook:
---|---|---
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 | [ ]
Hook callbacks execute inside the PoolManager's transaction context. Excessive gas consumption can make swaps revert or become economically unviable.
| 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 |
tstore/tload) for data that doesn't persist beyond the transaction. Requires Solidity >= 0.8.24 with EVM target set to cancun or later.string manipulation in callbacks; use bytes32 for identifiers.poolManager calls — repeated getSlot0() or getLiquidity() reads cost gas each time.# 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
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 |
| 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 |
Never do these things in a hook:
msg.sender for user identity - It's always PoolManagerbeforeSwapReturnDelta without understanding NoOp attackstransfer() for ETH - Use call{value:}("")block.timestamp for randomnesstx.origin for authorization - It's a phishing vector; malicious contracts can relay calls with the original user's tx.origin---|---|---
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.
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 |
Weekly Installs
288
Repository
GitHub Stars
185
First Seen
Feb 12, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex274
opencode272
gemini-cli269
github-copilot267
kimi-cli264
cursor264
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
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 |
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 |