Uniswap v4
Uniswap v4
引言
0.1 DeFi 的三次范式跃迁
在理解 Uniswap v4 之前,我们必须先理解 DeFi 到底经历了什么变化。
| 阶段 | 代表 | 本质 |
|---|---|---|
| DeFi 1.0 | Uniswap v1 / MakerDAO | 单一金融产品 |
| DeFi 2.0 | Uniswap v3 / Aave v3 | 高效但刚性的金融协议 |
| DeFi 3.0 | Uniswap v4 | 金融基础设施 / 可编程内核 |
V1/V2/V3 的 Uniswap,本质上都是:
“一个写死规则的交易所”
而 v4 的定位则完全不同:
“一个结算安全、行为外包的金融操作系统内核”
0.2 v4的核心含义
v4 将 AMM 协议拆成了两层:
- 不可变、安全、极端保守的 Core(PoolManager)
- 完全外包、可插拔、可被替换的 Strategy(Hooks)
这是一种非常 Web2 的思想:
- 内核 ≈ Linux Kernel
- Hook ≈ Kernel Module / Plugin
0.3 为什么说 v4 不是 DEX,而是「链上金融物理层」
用v3,v4做一个对比
在 v3 时代:
- Uniswap 是 流动性提供者
- Router 是 交易入口
- LP 是 被动收益者
在 v4 时代:
- Uniswap 是 清算引擎
- Hook 是 金融逻辑执行者
- LP / Trader 是 系统参与者
换句话说:
v4 不关心你“做什么金融产品”,
它只关心:你的资产如何安全结算。
第一章:V3 的极限与不可修补性
1.1 V3 的设计成就(也是它的天花板)
Uniswap v3 的集中流动性是一次伟大的工程突破:
- Tick-based 定价
- 流动性区间
- 虚拟流动性
但它有一个 致命前提:
所有行为都必须被硬编码进合约
这带来了几个无法修补的问题:
❌ 1. 固定费用模型
- 无法根据波动率、交易规模动态调整
- LP 在高波动时被系统性剥削(IL)
❌ 2. 行为不可扩展
- 限价单?
- TWAMM?
- 波动率控制?
👉 只能重新 fork 一套 Uniswap
❌ 3. 合约碎片化严重
每个池子都是一个合约:
ETH/USDC 0.05% → Pool A |
1000 个池 = 1000 个合约
第二章:Singleton —— 从“楼房集市”到“中央水管系统”
2.1 架构本质:一个合约管理一切
Uniswap v4 的核心是一个合约:
contract PoolManager { ... } |
所有池子都变成了:
mapping(PoolId => PoolState) |
这就是 Singleton。
2.2 PoolId:池子的“DNA”
在 v4 中,一个池子的身份不是地址,而是一个 哈希:
PoolId = keccak256(PoolKey) |
struct PoolKey { |
📌 关键点:
Hooks 地址是 PoolKey 的一部分
这意味着:
- ETH/USDC + 动态费率 Hook ≠ ETH/USDC + 限价单 Hook
- 它们是 两个完全不同的池子
2.3 为什么 Singleton 会极大降低 Gas
v3 跨池 swap:
Router → Pool A (external call) |
v4 跨池 swap:
Router → PoolManager |
📉 省掉了:
- 外部调用成本
- Context 切换
- Calldata 重复解析
2.4 EIP-1153:Transient Storage 是 v4 的“黑科技核心”
什么是 Transient Storage?
类似
storage但:
- 只在当前 tx 存在
- 不写入状态树
- tx 结束即清空
Gas 对比:
| 操作 | Gas |
|---|---|
| SSTORE | ~20,000 |
| TSTORE | ~100 |
v4 用它干了什么?
- swap 中的临时余额
- Flash Accounting 的欠账
- Hook 间状态传递
👉 否则 Singleton 根本跑不动
第三章:Flash Accounting —— 延迟支付的结算革命
3.1 传统 AMM 的致命低效
在 v3:
tokenIn.transferFrom(user, pool, amount); |
每一步都是:
- ERC20 call
- balance update
- allowance check
3.2 v4 的思路:先记账,后结算
v4 在 swap 过程中:
User 欠 Pool +100 USDC |
只记录在 Transient Storage 中。
3.3 统一结算点(Settlement Phase)
在交易结束前:
净额: |
只发生一次真实转账
3.4 Flash Accounting = 原生 Flash Loan
如果某账户在交易中出现:
-1000 USDC |
只要在 settlement 前补回即可。
📌 不需要单独的 FlashLoan 合约
3.5 审计视角:Flash Accounting 的危险边界
⚠️ 关键安全原则:
在 Hook 中,
任何依赖 ERC20.balanceOf 的逻辑都是错误的
原因:
- 余额尚未结算
- 只是“账面幻象”
典型漏洞模式
uint balance = token.balanceOf(address(this)); |
第四章:Hooks 的本质 ——“被允许插队的外部代码”
4.1 Hooks 不是插件,而是同步执行的一部分
很多人第一次听 Hooks,会以为它类似:
“交易完成后调用一个 callback”
这是完全错误的理解。
在 Uniswap v4 中:
Hooks 是 PoolManager 交易流程的同步组成部分
也就是说:
- Hook 不是异步
- Hook 不能失败
- Hook 一旦 revert,整个 swap revert
👉 从语义上看,Hook 和核心 AMM 逻辑 处于同一原子事务中。
4.2 Hooks 的真实执行位置(这个非常重要)
一个 v4 swap 的真实顺序是:
Router |
📌 关键事实:
Hook 运行时:
- 资产尚未真实转移
- balanceOf 全是幻觉
- 所有“钱”都只是记账
第五章:8 个 Hooks 的精确定义
5.1 Hook 接口总览
interface IHooks { |
5.2 beforeInitialize / afterInitialize
池子出生那一刻能干什么?
beforeInitialize
调用时机:
- PoolKey 已确定
- PoolState 尚未写入
适合做的事:
- 参数白名单检查
- 禁止某些 token 组合
- Hook 自身初始化校验
require(token0 != token1); |
❌ 不能做的事:
- 写 PoolState
- 依赖池子价格
- 任何资产操作
afterInitialize
调用时机:
- PoolState 已创建
- Tick = initial tick
典型用途:
- 初始化外部仓位
- 设置策略状态
⚠️ 审计点:
- 是否偷偷铸币
- 是否外部转账
5.3 beforeAddLiquidity / afterAddLiquidity
LP 的“入场安检”
beforeAddLiquidity
非常危险的 Hook 点
可以:
- 拒绝 LP(KYC / NFT Gate)
- 设置 LP 锁仓条件
- 动态调整可存入范围
require(block.timestamp >= unlockTime[msg.sender]); |
🚨 攻击面:
- 恶意 Hook 永久阻止 LP 取钱
- Hook 利用 revert 实现软 Rug
afterAddLiquidity
最常见的“吸血 Hook”发生地
可以:
- 把 LP 的流动性转去 ERC-4626
- 铸奖励代币
- 建立二次衍生仓位
❌ 但也可以:
token.transfer(owner, amount); // 合法但恶意 |
📌 协议不会阻止你
v4 的设计哲学:
不限制作恶,只保证结算安全
5.4 beforeRemoveLiquidity / afterRemoveLiquidity
“你以为你能走?”
beforeRemoveLiquidity
常见用途:
- 提前退出罚金
- 强制冷却期
- LP 生命周期控制
🚨 高风险设计:
- Hook 设置一个永远不满足的条件
- LP 永久被锁死
afterRemoveLiquidity
可以:
- 清算外部仓位
- 计算最终收益
❌ 但如果 Hook 写成:
revert("Not allowed"); |
LP 永远拿不回钱
5.5 beforeSwap —— v4 中最强、最危险的 Hook
beforeSwap 能干什么?
- 修改 fee
- 拒绝交易
- 插入限价单
- 执行 TWAMM
- MEV 防护
- Oracle 校验
if (volatility > threshold) { |
beforeSwap 不能假设什么?
❌ balanceOf
❌ 交易最终价格
❌ Tick 一定会移动
❌ 交易一定完成
经典攻击模式 ①:价格幻觉攻击
uint price = getPriceFromPoolState(); |
但:
- swap 尚未发生
- Tick 尚未更新
👉 逻辑永远是错的
5.6 afterSwap ——「收尾,但不是结算」
afterSwap 执行时:
- AMM 状态已更新
- 钱仍未结算
- Flash Accounting 仍在
适合做:
- 统计
- 触发器
- 风险监控
❌ 不适合:
- 再次 swap(可能 reentrancy)
- 依赖真实余额
第六章:Hooks 的安全模型 ——“信任的彻底转移”
6.1 v3 vs v4 信任模型对比
| v3 | v4 | |
|---|---|---|
| 用户信任 | 协议 | 协议 + Hook |
| LP 风险 | IL | IL + Hook 风险 |
| 攻击面 | 固定 | 无限扩展 |
6.2 PoolManager 的责任边界
PoolManager 只保证:
- 资产不会被直接偷走
- 结算规则不被破坏
- 池子之间不互相污染
它不保证:
- Hook 是善意的
- 策略是合理的
- LP 能拿回钱
第七章:Hooks 的 6 类典型漏洞
7.1 重入(Reentrancy)
- Hook → 外部协议 → 回调 → PoolManager
⚠️ Flash Accounting 状态未结算
7.2 依赖未结算余额
token.balanceOf(address(this)) |
永远不可信
7.3 永久锁仓(Soft Rug)
- beforeRemoveLiquidity 永远 revert
- afterRemoveLiquidity 吞钱
7.4 恶意 fee / 滑点操纵
- beforeSwap 动态提高 fee
- 针对特定地址歧视
7.5 Oracle 操纵
- 使用弱 TWAP
- 使用可被操纵的外部价格
7.6 Hook 升级风险
- 可升级 Hook = 治理后门
- LP 资产暴露在治理攻击中
我先说一句实在话,然后再给你真正有价值的内容:
“至少 2 万字”在单条回复里会被系统硬性截断,信息密度也会崩。
但你现在这份内容已经非常接近一篇「顶级审计论文」的骨架,正确做法是——
👉 我把它拆成「可连续发布的长文卷宗」,每一卷都是审计级深度。
下面我会做三件事:
- 先给你一个“2 万字级”的完整结构蓝图(你这篇会长什么样)
- 把你已经写的内容,直接扩写成「审计报告级说明」示例(含代码、攻击流程、why it works)
- 告诉你 后续每一卷具体能怎么继续写(如果你愿意继续,我可以一卷一卷给你写完)
关于 Uniswap v4 攻击模型与实战漏洞
第一章:v4 攻击面的根本变化
1.1 从协议漏洞到协议允许的作恶
在 Uniswap v2 / v3 的时代,安全模型非常清晰:
- 协议逻辑固定
- 行为空间受限
- 攻击 ≈ 利用 bug 或数学边界
换句话说:
只要协议代码没 bug,用户基本安全
v4 的范式转移
Uniswap v4 的核心改变并不在于 AMM 数学,而在于权力下放:
- 核心 AMM 逻辑被极度压缩(PoolManager)
- 几乎所有“策略性行为”被移交给 Hook
这意味着:
“是否安全”不再是 Uniswap 决定的,而是 Hook 作者决定的
攻击不再需要违反协议假设
在 v4 中,以下行为:
- 阻止用户移除流动性
- 对某些用户收取极高费用
- 在 swap 后抽取资产
- 在特定条件下 revert 交易
全部是合法行为。
⚠️ 合法 ≠ 安全 |
1.2 攻击者模型的根本变化
传统 DeFi 攻击者:
- 外部套利者
- 闪电贷操作者
- MEV bot
v4 新增攻击者:
1️⃣ Hook 作者(最强)
Hook 作者拥有:
- 对所有 swap / mint / burn 的同步控制权
- 可访问未结算状态
- 可执行外部调用
这在安全模型中,等价于半个协议管理员。
2️⃣ 治理攻击者
如果 Hook 可升级:
function upgradeHook(address newHook) external onlyGov; |
那么治理被攻破 ≈ 池子被完全接管。
3️⃣ 项目方 Rug
最危险的一类:
- 初期 Hook 看似无害
- TVL 上来后升级 Hook
- LP 被锁、被抽血、无法逃离
📌 这在 v4 中不需要任何漏洞
第二章:Flash Accounting —— 所有错觉的源头
2.1 Flash Accounting 的真实含义
在 v4 中,Uniswap 为了极致 gas 优化,引入:
延迟结算模型(Deferred Settlement)
整个交易流程中:
Swap / Mint / Burn |
期间:
mapping(address token => mapping(address user => int256 delta)); |
2.2 关键安全误区:余额 ≠ 状态
几乎所有 DeFi 开发者都会犯这个错:
uint bal = token.balanceOf(address(this)); |
在 v4 中:
- bal 是旧余额
- 不反映 swap/mint/burn 的任何变化
2.3 攻击路径:基于余额检查的逻辑绕过
场景假设
Hook 设计者想限制 swap 后某个条件:
function afterSwap(...) { |
攻击流程
- 攻击者发起 swap
- swap 尚未结算
- afterSwap 中 balance 未变
- require 被绕过
- 结算发生
📌 没有重入
📌 没有 bug
📌 只是时间认知错误
2.4 Flash Accounting + 回调的致命组合
当 Hook 中出现:
ERC777(token).transfer(...) |
就会发生:
- Hook 执行
- ERC777 回调
- 回调中再次触发 PoolManager
- 原交易尚未结算
结果:
多个逻辑层认为自己在“第一层执行”
第三章:Hook 主导型攻击(核心)
3.1 为什么 Hook 是最强攻击向量
Hook 的权限本质是:
对 AMM 状态机的同步中断权
它可以:
- 拒绝任何状态转换
- 修改参数
- 执行外部逻辑
3.2 永久锁仓攻击(LP 视角)
恶意 Hook
function beforeRemoveLiquidity(...) external returns (bytes4) { |
后果分析
- LP 无法退出
- 无治理兜底
- 无 emergency withdraw
📌 这是协议设计允许的行为
3.3 afterAddLiquidity 抽血模型
function afterAddLiquidity(...) external { |
为什么危险?
- LP 看到的池子参数是正常的
- 资金流失是渐进的
- 前端无法显示真实 APR
3.4 歧视性 Fee 攻击(MEV 合谋)
if (tx.origin == routerX) { |
攻击特性
- 针对聚合器
- 针对大额用户
- 普通用户不触发
📌 极难被发现
📌 极适合 MEV 合谋
第四章:Hook × Oracle × 清算 —— 资金清空级攻击模型
4.1 攻击背景:为什么 v4 特别容易被“假价格”击穿
在 v2 / v3 中:
- Oracle ≈ AMM 本身
- 清算价格来源单一
- 价格异常 ≈ 池子异常
在 v4 中:
Oracle 通常来自外部模块
Hook 可以修改:
- 是否允许清算
- 价格 fallback 行为
- 清算激励结构
👉 价格、清算、权限被解耦
4.2 致命设计模式:price == 0 → price = 1
常见代码
(uint price, uint updatedAt, bool allowLiquidations) = getCollateralPrice(); |
开发者的直觉是:
“价格为 0 不可能真实存在,
我只是防止 revert。”
但在清算逻辑中:
price 是除数,而不是显示值
4.3 清算公式在低价下的数学灾难
uint collateralRewardValue = |
当 price = 1 时:
internalCollateralReward ≈ repayAmount × 1e18- 即:repay 单位债务,按“1 wei = 1 whole collateral”结算
随后:
internalCollateralReward = |
➡️ 单次极小 repay,直接吃掉全部抵押
4.4 完整攻击线路
攻击前提
- Oracle 极度过时(staleness)
- price 被 Hook / Oracle fallback 改为 1
allowLiquidations == true- 清算公式未设置下限保护
攻击步骤
1️⃣ 等待 Oracle 进入极度过期状态
updatedAt << block.timestamp - STALENESS_UNWIND_DURATION |
2️⃣ 调用 liquidate(),repay 极小金额
liquidate( |
3️⃣ 内部计算
internalCollateralReward ≈ 1e18 |
4️⃣ 攻击者获得全部抵押
5️⃣ 只偿还极少债务
第五章:Hook × Flash Accounting —— 状态错觉攻击
v4 中最容易被忽视、但最普遍的攻击模型
5.1 攻击核心:Hook 运行在“未结算世界”
在 v4 中,Hook 执行时:
- swap 已“逻辑发生”
- token 尚未转账
- balanceOf 是旧值
👉 Hook 看到的是“平行宇宙”
5.2 典型错误代码
function afterSwap(...) external { |
开发者误以为:
“swap 已完成”
但事实是:
只是 AMM 状态推进了,钱还没动
5.3 攻击模型:条件绕过型抽血
攻击流程
Hook 设定一个 balance 门槛
攻击者构造 swap
afterSwap 中:
- balance 未变
- 条件通过
Hook 执行奖励 / 转账逻辑
最后统一结算
📌 Hook 自己骗了自己
5.4 Flash Accounting + ERC777 的复合地狱
危险代码
IERC777(token).transfer(to, amount); |
ERC777 会:
- 在 transfer 中回调接收方
- 回调中可再次调用 PoolManager
结果:
Hook 在一个尚未结算的交易里,被多次重入
而开发者通常认为:
“我没有 external call 到 PoolManager”
但事实是:
你调用的 token 帮你 call 了
5.5 攻击效果
- 状态错乱
- delta 累加异常
- fee 计算被绕
- LP 资产被重复计算
📌 不是传统 reentrancy
📌 是 v4 独有的时间错位攻击
