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
ETH/USDC 0.3% → Pool B
ETH/DAI 0.3% → Pool C

1000 个池 = 1000 个合约


第二章:Singleton —— 从“楼房集市”到“中央水管系统”

2.1 架构本质:一个合约管理一切

Uniswap v4 的核心是一个合约:

contract PoolManager { ... }

所有池子都变成了:

mapping(PoolId => PoolState)

这就是 Singleton


2.2 PoolId:池子的“DNA”

在 v4 中,一个池子的身份不是地址,而是一个 哈希

PoolId = keccak256(PoolKey)
struct PoolKey {
Currency currency0;
Currency currency1;
uint24 fee;
int24 tickSpacing;
IHooks hooks;
}

📌 关键点

Hooks 地址是 PoolKey 的一部分

这意味着:

  • ETH/USDC + 动态费率 Hook ≠ ETH/USDC + 限价单 Hook
  • 它们是 两个完全不同的池子

2.3 为什么 Singleton 会极大降低 Gas

v3 跨池 swap:

Router → Pool A (external call)
→ Pool B (external call)
→ Pool C (external call)

v4 跨池 swap:

Router → PoolManager
├─ swap(poolA)
├─ swap(poolB)
└─ swap(poolC)

📉 省掉了:

  • 外部调用成本
  • 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);
tokenOut.transfer(user, amountOut);

每一步都是:

  • ERC20 call
  • balance update
  • allowance check

3.2 v4 的思路:先记账,后结算

v4 在 swap 过程中:

User 欠 Pool +100 USDC
Pool 欠 User -0.05 ETH

只记录在 Transient Storage 中。


3.3 统一结算点(Settlement Phase)

在交易结束前:

净额:
User → Pool : 100 USDC
Pool → User : 0.05 ETH

只发生一次真实转账


3.4 Flash Accounting = 原生 Flash Loan

如果某账户在交易中出现:

-1000 USDC

只要在 settlement 前补回即可。

📌 不需要单独的 FlashLoan 合约


3.5 审计视角:Flash Accounting 的危险边界

⚠️ 关键安全原则

在 Hook 中,
任何依赖 ERC20.balanceOf 的逻辑都是错误的

原因:

  • 余额尚未结算
  • 只是“账面幻象”

典型漏洞模式

uint balance = token.balanceOf(address(this));
require(balance >= x); // ❌ 永远不可信

第四章:Hooks 的本质 ——“被允许插队的外部代码”

4.1 Hooks 不是插件,而是同步执行的一部分

很多人第一次听 Hooks,会以为它类似:

“交易完成后调用一个 callback”

这是完全错误的理解

在 Uniswap v4 中:

Hooks 是 PoolManager 交易流程的同步组成部分

也就是说:

  • Hook 不是异步
  • Hook 不能失败
  • Hook 一旦 revert,整个 swap revert

👉 从语义上看,Hook 和核心 AMM 逻辑 处于同一原子事务中


4.2 Hooks 的真实执行位置(这个非常重要)

一个 v4 swap 的真实顺序是:

Router
└─ PoolManager.swap()
├─ beforeSwap() ← Hook
├─ AMM 核心计算
├─ afterSwap() ← Hook
└─ Flash Accounting Settlement

📌 关键事实

Hook 运行时:

  • 资产尚未真实转移
  • balanceOf 全是幻觉
  • 所有“钱”都只是记账

第五章:8 个 Hooks 的精确定义

5.1 Hook 接口总览

interface IHooks {
function beforeInitialize(...) external returns (bytes4);
function afterInitialize(...) external returns (bytes4);

function beforeAddLiquidity(...) external returns (bytes4);
function afterAddLiquidity(...) external returns (bytes4);

function beforeRemoveLiquidity(...) external returns (bytes4);
function afterRemoveLiquidity(...) external returns (bytes4);

function beforeSwap(...) external returns (bytes4);
function afterSwap(...) external returns (bytes4);
}

5.2 beforeInitialize / afterInitialize

池子出生那一刻能干什么?

beforeInitialize

调用时机:

  • PoolKey 已确定
  • PoolState 尚未写入

适合做的事:

  • 参数白名单检查
  • 禁止某些 token 组合
  • Hook 自身初始化校验
require(token0 != token1);
require(allowed[msg.sender]);

不能做的事:

  • 写 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) {
fee = highFee;
}

beforeSwap 不能假设什么?

balanceOf
❌ 交易最终价格
❌ Tick 一定会移动
❌ 交易一定完成


经典攻击模式 ①:价格幻觉攻击

uint price = getPriceFromPoolState();
require(price > x);

但:

  • 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 万字”在单条回复里会被系统硬性截断,信息密度也会崩。
但你现在这份内容已经非常接近一篇「顶级审计论文」的骨架,正确做法是——
👉 我把它拆成「可连续发布的长文卷宗」,每一卷都是审计级深度。

下面我会做三件事:

  1. 先给你一个“2 万字级”的完整结构蓝图(你这篇会长什么样)
  2. 把你已经写的内容,直接扩写成「审计报告级说明」示例(含代码、攻击流程、why it works)
  3. 告诉你 后续每一卷具体能怎么继续写(如果你愿意继续,我可以一卷一卷给你写完)

关于 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

Hook 回调

Hook 回调

……

统一结算

期间:

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(...) {
require(token.balanceOf(address(this)) >= threshold);
}

攻击流程

  1. 攻击者发起 swap
  2. swap 尚未结算
  3. afterSwap 中 balance 未变
  4. require 被绕过
  5. 结算发生

📌 没有重入
📌 没有 bug
📌 只是时间认知错误


2.4 Flash Accounting + 回调的致命组合

当 Hook 中出现:

ERC777(token).transfer(...)

就会发生:

  1. Hook 执行
  2. ERC777 回调
  3. 回调中再次触发 PoolManager
  4. 原交易尚未结算

结果:

多个逻辑层认为自己在“第一层执行”


第三章:Hook 主导型攻击(核心)

3.1 为什么 Hook 是最强攻击向量

Hook 的权限本质是:

对 AMM 状态机的同步中断权

它可以:

  • 拒绝任何状态转换
  • 修改参数
  • 执行外部逻辑

3.2 永久锁仓攻击(LP 视角)

恶意 Hook

function beforeRemoveLiquidity(...) external returns (bytes4) {
revert("Liquidity removal disabled");
}

后果分析

  • LP 无法退出
  • 无治理兜底
  • 无 emergency withdraw

📌 这是协议设计允许的行为


3.3 afterAddLiquidity 抽血模型

function afterAddLiquidity(...) external {
uint skim = computeSkim();
token0.transfer(owner, skim);
}

为什么危险?

  • LP 看到的池子参数是正常的
  • 资金流失是渐进的
  • 前端无法显示真实 APR

3.4 歧视性 Fee 攻击(MEV 合谋)

if (tx.origin == routerX) {
feeBps = 5000;
}

攻击特性

  • 针对聚合器
  • 针对大额用户
  • 普通用户不触发

📌 极难被发现
📌 极适合 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();

if (price == 0) {
price = 1; // 防止除零
}

开发者的直觉是:

“价格为 0 不可能真实存在,
我只是防止 revert。”

但在清算逻辑中:

price 是除数,而不是显示值


4.3 清算公式在低价下的数学灾难

uint collateralRewardValue =
repayAmount * (10000 + liqIncentiveBps) / 10000;

uint internalCollateralReward =
collateralRewardValue * 1e18 / price;

price = 1 时:

  • internalCollateralReward ≈ repayAmount × 1e18
  • 即:repay 单位债务,按“1 wei = 1 whole collateral”结算

随后:

internalCollateralReward =
min(internalCollateralReward, collateralBalance);

➡️ 单次极小 repay,直接吃掉全部抵押


4.4 完整攻击线路

攻击前提

  1. Oracle 极度过时(staleness)
  2. price 被 Hook / Oracle fallback 改为 1
  3. allowLiquidations == true
  4. 清算公式未设置下限保护

攻击步骤

1️⃣ 等待 Oracle 进入极度过期状态

updatedAt << block.timestamp - STALENESS_UNWIND_DURATION

2️⃣ 调用 liquidate(),repay 极小金额

liquidate(
borrower,
repayAmount = 1,
minCollateralOut = 0
);

3️⃣ 内部计算

internalCollateralReward ≈ 1e18
→ min(collateralBalance)
→ 全部抵押

4️⃣ 攻击者获得全部抵押

5️⃣ 只偿还极少债务

第五章:Hook × Flash Accounting —— 状态错觉攻击

v4 中最容易被忽视、但最普遍的攻击模型


5.1 攻击核心:Hook 运行在“未结算世界”

在 v4 中,Hook 执行时:

  • swap 已“逻辑发生”
  • token 尚未转账
  • balanceOf 是旧值

👉 Hook 看到的是“平行宇宙”


5.2 典型错误代码

function afterSwap(...) external {
uint bal = token.balanceOf(address(this));
require(bal >= minRequired);
}

开发者误以为:

“swap 已完成”

但事实是:

只是 AMM 状态推进了,钱还没动


5.3 攻击模型:条件绕过型抽血

攻击流程

  1. Hook 设定一个 balance 门槛

  2. 攻击者构造 swap

  3. afterSwap 中:

    • balance 未变
    • 条件通过
  4. Hook 执行奖励 / 转账逻辑

  5. 最后统一结算

📌 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 独有的时间错位攻击