Uniswap v4
Uniswap v4
引言
0.1 DeFi 的三次范式跃迁
在理解 Uniswap v4 之前,我们必须先理解 DeFi 到底经历了什么变化。DeFi(去中心化金融)从最初的简单借贷和交易,到如今的复杂金融基础设施,其演进反映了区块链技术的成熟和用户需求的增长。
| 阶段 | 代表 | 本质 |
|---|---|---|
| DeFi 1.0 | Uniswap v1 / MakerDAO | 单一金融产品 |
| DeFi 2.0 | Uniswap v3 / Aave v3 | 高效但刚性的金融协议 |
| DeFi 3.0 | Uniswap v4 | 金融基础设施 / 可编程内核 |
DeFi 1.0 时代,协议专注于解决特定问题,如 Uniswap v1 的代币交换或 MakerDAO 的稳定币借贷。这些产品虽然开创性,但功能单一,无法扩展到更复杂的金融场景。
DeFi 2.0 引入了效率提升,如 Uniswap v3 的集中流动性,允许 LP 在特定价格区间提供流动性,减少了资本浪费。然而,这些协议仍然高度刚性,所有逻辑都硬编码在合约中,难以适应市场变化或添加新功能。
现在,DeFi 3.0 以 Uniswap v4 为代表,标志着从“产品”向“基础设施”的转变。v4 不再是一个固定的 DEX,而是提供了一个可编程的内核,允许开发者通过 Hooks 自定义金融逻辑。这不仅提高了灵活性,还开启了无限的可能性,如动态费用、限价单、MEV 防护等。
V1/V2/V3 的 Uniswap,本质上都是:
“一个写死规则的交易所”
而 v4 的定位则完全不同:
“一个结算安全、行为外包的金融操作系统内核”
这种转变意味着开发者可以构建更丰富的 DeFi 应用,而无需从头开发 AMM 核心,从而加速创新并降低风险。
0.2 v4 的核心含义
v4 将 AMM 协议拆成了两层:
- 不可变、安全、极端保守的 Core(PoolManager)
- 完全外包、可插拔、可被替换的 Strategy(Hooks)
这种架构设计深受操作系统内核启发。PoolManager 就像 Linux 内核,负责最核心的资源管理和安全保障,确保资产结算的正确性和原子性。它不关心具体的金融逻辑是什么,只保证交易的执行是安全的。
Hooks 则类似于内核模块或插件,允许外部开发者注入自定义逻辑。这些逻辑可以是动态费用调整、限价单执行、风险控制等。通过 Hooks,v4 实现了高度的可扩展性:同一个 PoolManager 可以支持无数种不同的金融产品,而无需修改核心代码。
这是一种非常 Web2 的思想:
- 内核 ≈ Linux Kernel
- Hook ≈ Kernel Module / Plugin
在 Web2 中,操作系统提供基础服务,应用通过 API 扩展功能。v4 将这一理念带入 DeFi,使得协议不再是封闭的,而是开放的生态系统。开发者可以专注于创新金融产品,而 Uniswap 提供底层保障。
0.3 为什么说 v4 不是 DEX,而是「链上金融物理层」
用 v3 和 v4 做一个对比。
在 v3 时代:
- Uniswap 是 流动性提供者
- Router 是 交易入口
- LP 是 被动收益者
Uniswap v3 主要关注于提供高效的代币交换服务,通过集中流动性和动态费用来优化资本利用率。LP 通过提供流动性赚取手续费,但收益受制于无常损失和市场波动。
在 v4 时代:
- Uniswap 是 清算引擎
- Hook 是 金融逻辑执行者
- LP / Trader 是 系统参与者
v4 的角色发生了根本转变。它不再局限于交换功能,而是成为一个通用的金融结算平台。PoolManager 负责所有资产的原子结算,确保交易的安全性和一致性。而具体的金融行为,如限价单、杠杆交易或衍生品,则通过 Hooks 实现。
换句话说:
v4 不关心你”做什么金融产品”,
它只关心:你的资产如何安全结算。
这种设计使得 v4 成为区块链上的“金融物理层”,类似于 TCP/IP 协议栈中的网络层,提供基础传输服务,而上层应用可以无限扩展。开发者可以基于 v4 构建复杂的 DeFi 协议,如期权市场、杠杆农场或跨链桥,而无需担心底层结算的风险。
第一章:V3 的极限与不可修补性
1.1 V3 的设计成就(也是它的天花板)
Uniswap v3 的集中流动性是一次伟大的工程突破:
- Tick-based 定价
- 流动性区间
- 虚拟流动性
v3 引入了 Tick 系统,将价格范围划分为离散的区间,LP 可以选择在特定区间提供流动性。这不仅提高了资本效率,还允许更精细的价格控制。虚拟流动性概念进一步优化了数学模型,使池子能够处理更大范围的价格变动。
但它有一个 致命前提:
所有行为都必须被硬编码进合约
这意味着任何新功能,如动态费用或限价单,都需要通过治理或分叉来实现,导致开发周期长、风险高。
这带来了几个无法修补的问题:
❌ 1. 固定费用模型
- 无法根据波动率、交易规模动态调整
- LP 在高波动时被系统性剥削(IL)
固定费用无法适应市场条件,高波动期手续费可能不足以补偿 LP 的无常损失。
❌ 2. 行为不可扩展
- 限价单?
- TWAMM?
- 波动率控制?
👉 只能重新 fork 一套 Uniswap
每次添加新功能都需要创建新合约,增加了碎片化和复杂性。
❌ 3. 合约碎片化严重
每个池子都是一个合约:
ETH/USDC 0.05% → Pool A |
1000 个池 = 1000 个合约
这不仅增加了部署成本,还使得跨池操作复杂,Gas 费用高昂。
第二章:Singleton —— 从”楼房集市”到”中央水管系统”
2.1 架构本质:一个合约管理一切
Uniswap v4 的核心是一个合约:
contract PoolManager { ... } |
代码说明:PoolManager 是 v4 的核心合约,它使用 Singleton 模式,通过一个合约实例管理所有流动性池的状态,而不是像 v3 那样为每个池子部署单独合约。这大大降低了 Gas 成本,因为避免了多次外部调用。
以下是一个简化的 PoolManager swap 方法示例:
function swap(PoolKey memory key, IPoolManager.SwapParams memory params) external returns (BalanceDelta delta) { |
代码说明:这个 swap 方法展示了 v4 的核心逻辑:验证调用者、计算交换量、更新状态、记录债务,并调用 Hooks。实际代码更复杂,包括 slippage 检查和事件发射。
所有池子都变成了:
mapping(PoolId => PoolState) |
代码说明:这里使用一个映射(mapping)来存储所有池子的状态,PoolId 是池子的唯一标识符,PoolState 包含池子的所有必要数据,如流动性、价格等。这种设计使得跨池操作更加高效。
Singleton 架构的核心优势在于集中管理。它允许 PoolManager 在一次交易中处理多个池子的操作,而无需外部调用。这不仅节省 Gas,还提高了交易的原子性:要么全部成功,要么全部回滚。
这就是 Singleton。
2.2 PoolId:池子的”DNA”
在 v4 中,一个池子的身份不是地址,而是一个 哈希:
PoolId = keccak256(PoolKey) |
代码说明:PoolId 通过对 PoolKey 进行 keccak256 哈希计算得到,确保每个池子有唯一的标识符。这使得池子可以动态创建,而不需要预先部署合约。
struct PoolKey { |
代码说明:PoolKey 结构体定义了池子的关键参数,包括两个代币(currency0 和 currency1)、手续费(fee)、tick 间距(tickSpacing)和 Hooks 合约地址。Hooks 地址作为参数的一部分,使得不同 Hooks 的池子被视为不同池子。
📌 关键点:
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 根本跑不动
以下是使用 Transient Storage 的示例代码:
// 使用 EIP-1153 Transient Storage |
代码说明:tstore 和 tload 是 Transient Storage 的操作函数,用于记录和读取临时债务。数据只在当前交易中存在,结束后自动清空,避免了状态树的写入开销。
第三章:Flash Accounting —— 延迟支付的结算革命
3.1 传统 AMM 的致命低效
在 v3:
tokenIn.transferFrom(user, pool, amount); |
每一步都是:
- ERC20 call
- balance update
- allowance check
传统 AMM 在每次交易中都需要立即执行代币转账。这涉及到多次外部调用 ERC20 合约,检查余额和授权,导致高 Gas 成本和潜在的重入风险。此外,多次转账还可能引入不一致性,如果某一步失败,整个交易可能部分执行。
3.2 v4 的思路:先记账,后结算
v4 在 swap 过程中:
User 欠 Pool +100 USDC |
只记录在 Transient Storage 中。
v4 采用延迟结算策略:在交易执行期间,不立即转账代币,而是记录净债务变化。这利用了 Transient Storage,只在当前交易中有效,避免了状态树的写入。用户和池子之间的债务用正负数表示,确保最终净额正确。
3.3 统一结算点(Settlement Phase)
在交易结束前:
净额: |
只发生一次真实转账
以下是简化的结算逻辑代码:
function settle() external { |
代码说明:settle 函数在交易结束后执行,计算并结算净债务。正数表示用户欠池子,负数表示池子欠用户。只执行必要的转账,然后清空 Transient Storage 中的记录。
所有操作完成后,PoolManager 计算净债务,并执行最终的代币转账。这减少了外部调用次数,提高了效率,并确保了原子性:如果结算失败,整个交易回滚。
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 逻辑 处于同一原子事务中。
Hooks 的同步特性意味着它们与 AMM 计算紧密集成,不能独立失败。这确保了交易的完整性,但也要求 Hook 开发者小心处理错误:任何 revert 都会终止整个操作。此外,Hooks 运行在未结算状态下,必须避免依赖实时余额,以防逻辑错误。
4.2 Hooks 的真实执行位置(这个非常重要)
一个 v4 swap 的真实顺序是:
Router |
📌 关键事实:
Hook 运行时:
- 资产尚未真实转移
- balanceOf 全是幻觉
- 所有”钱”都只是记账
第五章:8 个 Hooks 的精确定义
5.1 Hook 接口总览
interface IHooks { |
代码说明:IHooks 接口定义了 8 个 Hook 函数,每个函数在池子生命周期的不同阶段被调用。before* 函数在操作前执行,可以修改参数或拒绝操作;after* 函数在操作后执行,用于后续处理。返回值 bytes4 是函数选择器,用于验证实现。
以下是一个简单的 Hook 实现示例,实现动态费用:
contract DynamicFeeHook is IHooks { |
代码说明:这个 Hook 在 beforeSwap 中根据波动率动态调整手续费,在 afterSwap 中发射事件记录交易。其他 Hook 函数返回默认选择器,表示不执行额外逻辑。
5.2 beforeInitialize / afterInitialize
池子出生那一刻能干什么?
beforeInitialize
调用时机:
- PoolKey 已确定
- PoolState 尚未写入
适合做的事:
- 参数白名单检查
- 禁止某些 token 组合
- Hook 自身初始化校验
require(token0 != token1); |
代码说明:在 beforeInitialize 中,可以使用 require 语句检查条件。例如,确保两个代币不同(避免无效池子),或检查调用者是否在允许列表中。这可以防止恶意或无效的池子创建。
参数: PoolKey calldata key - 包含池子的所有配置信息。
返回值: bytes4 - 必须是 IHooks.beforeInitialize.selector。
安全注意: 不能修改 PoolKey 或写入任何状态,只能进行校验。如果校验失败,整个初始化 revert。
afterInitialize
调用时机:
- PoolState 已创建
- Tick = initial tick
典型用途:
- 初始化外部仓位
- 设置策略状态
⚠️ 审计点:
- 是否偷偷铸币
- 是否外部转账
代码说明:afterInitialize 在池子状态创建后调用,可以执行初始化逻辑,如设置外部合约的状态或铸造初始代币。
参数: PoolKey calldata key - 池子配置。
返回值: bytes4 - 必须是 IHooks.afterInitialize.selector。
安全注意: 可以执行外部调用,但需小心重入。审计时检查是否滥用权限,如铸造代币给攻击者。
5.3 beforeAddLiquidity / afterAddLiquidity
LP 的”入场安检”
beforeAddLiquidity
非常危险的 Hook 点
可以:
- 拒绝 LP(KYC / NFT Gate)
- 设置 LP 锁仓条件
- 动态调整可存入范围
require(block.timestamp >= unlockTime[msg.sender]); |
代码说明:这个 Hook 可以根据条件拒绝流动性添加。例如,检查用户是否持有特定 NFT,或时间锁仓到期。
参数: PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params - 池子配置和流动性参数。
返回值: bytes4 - 必须是 IHooks.beforeAddLiquidity.selector。
安全注意: 过度限制可能导致 LP 无法退出。恶意 Hook 可永久锁仓资金。
🚨 攻击面:
- 恶意 Hook 永久阻止 LP 取钱
- Hook 利用 revert 实现软 Rug
afterAddLiquidity
最常见的”吸血 Hook”发生地
可以:
- 把 LP 的流动性转去 ERC-4626
- 铸奖励代币
- 建立二次衍生仓位
❌ 但也可以:
token.transfer(owner, amount); // 合法但恶意 |
代码说明:在流动性添加后,可以执行奖励逻辑,如铸造治理代币或存入收益农场。
参数: PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params, BalanceDelta delta - 包括流动性变化。
返回值: bytes4 - 必须是 IHooks.afterAddLiquidity.selector。
安全注意: 检查是否转移 LP 资金到外部合约。审计奖励机制是否公平。
📌 协议不会阻止你
v4 的设计哲学:
不限制作恶,只保证结算安全
5.4 beforeRemoveLiquidity / afterRemoveLiquidity
“你以为你能走?”
beforeRemoveLiquidity
常见用途:
- 提前退出罚金
- 强制冷却期
- LP 生命周期控制
🚨 高风险设计:
- Hook 设置一个永远不满足的条件
- LP 永久被锁死
代码说明:可以实施退出限制,如收取罚金或要求等待期。
参数: PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params - 流动性移除参数。
返回值: bytes4 - 必须是 IHooks.beforeRemoveLiquidity.selector。
安全注意: 确保条件合理,否则 LP 资金被锁。检查是否与治理结合使用。
afterRemoveLiquidity
可以:
- 清算外部仓位
- 计算最终收益
❌ 但如果 Hook 写成:
revert("Not allowed"); |
LP 永远拿不回钱
代码说明:在移除后,可以执行清理逻辑,如关闭外部仓位。
参数: PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params, BalanceDelta delta - 包括移除的流动性。
返回值: bytes4 - 必须是 IHooks.afterRemoveLiquidity.selector。
安全注意: 不能 revert 来阻止移除,只能执行后续逻辑。审计外部调用是否安全。
5.5 beforeSwap —— v4 中最强、最危险的 Hook
beforeSwap 能干什么?
- 修改 fee
- 拒绝交易
- 插入限价单
- 执行 TWAMM
- MEV 防护
- Oracle 校验
if (volatility > threshold) { |
代码说明:Hook 可以动态调整手续费,或实现复杂逻辑如限价单。
参数: PoolKey calldata key, IPoolManager.SwapParams calldata params - 交换参数。
返回值: bytes4 - 必须是 IHooks.beforeSwap.selector。
安全注意: 不能依赖未更新的状态。修改参数需谨慎。
beforeSwap 不能假设什么?
❌ balanceOf
❌ 交易最终价格
❌ Tick 一定会移动
❌ 交易一定完成
经典攻击模式 ①:价格幻觉攻击
uint price = getPriceFromPoolState(); |
但:
- swap 尚未发生
- Tick 尚未更新
👉 逻辑永远是错的
5.6 afterSwap ——“收尾,但不是结算”
afterSwap 执行时:
- AMM 状态已更新
- 钱仍未结算
- Flash Accounting 仍在
适合做:
- 统计
- 触发器
- 风险监控
❌ 不适合:
- 再次 swap(可能 reentrancy)
- 依赖真实余额
代码说明:用于记录统计或触发外部事件。
参数: PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta delta - 包括交换结果。
返回值: bytes4 - 必须是 IHooks.afterSwap.selector。
安全注意: 避免重入。不能修改结算。
第六章: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 资产暴露在治理攻击中
关于 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)); |
代码说明:这个映射记录了每个用户对每个代币的净债务或债权。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
📌 只是时间认知错误
以下是易受攻击的 Hook 代码示例:
function afterSwap(PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta delta) external { |
代码说明:这个 Hook 错误地使用了 balanceOf,在 Flash Accounting 下余额尚未更新,导致条件检查失效,可能允许不当的奖励发放。
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) { |
代码说明:这个 Hook 函数通过 revert 永久阻止流动性移除,导致 LP 无法取回资金。这是一种”软 Rug”攻击,利用协议允许的合法行为实现恶意目的。
后果分析
- 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 时,除法结果极大,可能导致攻击者获得全部抵押。
当 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,可能导致重入攻击,尤其在 Flash Accounting 未结算的状态下。
ERC777 会:
- 在 transfer 中回调接收方
- 回调中可再次调用 PoolManager
结果:
Hook 在一个尚未结算的交易里,被多次重入
而开发者通常认为:
“我没有 external call 到 PoolManager”
但事实是:
你调用的 token 帮你 call 了
5.5 攻击效果
- 状态错乱
- delta 累加异常
- fee 计算被绕
- LP 资产被重复计算
📌 不是传统 reentrancy
📌 是 v4 独有的时间错位攻击
