Uniswap V3 Clone 安全审计报告
概述
- 审计工具: Slither (v0.11.5)
- 审计日期: 2026年3月25日
- 审计范围:
src/目录下的核心合约 - 发现问题数: 62 个
- 高危问题数: 3 个
一、Slither 静态分析执行
1.1 安装 Slither
# macOS 使用 pipx 安装
pipx install slither-analyzer
1.2 运行分析
cd /Users/lianwenhua/indie/web3/uniswap/uniswap-v3-clone
forge build # 先编译项目
slither . # 运行静态分析
1.3 分析结果摘要
INFO:Slither:. analyzed (16 contracts with 101 detectors), 62 result(s) found
二、高危漏洞详情
2.1 arbitrary-send-erc20(任意地址转账漏洞)
漏洞描述
UniswapV3Manager 的回调函数中 transferFrom 使用了任意 from 地址,攻击者可以伪造 data 参数,将 player 设为其他用户地址,从而盗取他人代币。
Slither 检测输出
Detector: arbitrary-send-erc20
UniswapV3Manager.uniswapV3MintCallback(uint256,uint256,bytes) (src/UniswapV3Manager.sol#33-47) uses arbitrary from in transferFrom:
IERC20(extraData.token0).transferFrom(extraData.player,msg.sender,amount0)
UniswapV3Manager.uniswapV3SwapCallback(int256,int256,bytes) (src/UniswapV3Manager.sol#49-68) uses arbitrary from in transferFrom:
IERC20(extraData.token1).transferFrom(extraData.player,msg.sender,uint256(amount1Delta))
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#arbitrary-from-in-transferfrom
漏洞代码(修复前)
// src/UniswapV3Manager.sol
function uniswapV3MintCallback(
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
UniswapV3Pool.CallbackData memory extraData = abi.decode(
data,
(UniswapV3Pool.CallbackData)
);
// 🚨 漏洞:extraData.player 来自不可信的 data 参数
// 攻击者可以伪造 data,将 player 设为受害者地址
IERC20(extraData.token0).transferFrom(extraData.player, msg.sender, amount0);
IERC20(extraData.token1).transferFrom(extraData.player, msg.sender, amount1);
}
攻击场景
攻击者调用 Manager.mint(),传入伪造的 data:
┌─────────────────────────────────────────────────────────────┐
│ data = encode(CallbackData { │
│ token0: USDC, │
│ token1: WETH, │
│ player: 受害者地址, // ← 伪造为受害者地址 │
│ }) │
└─────────────────────────────────────────────────────────────┘
↓
Pool 回调 Manager.uniswapV3MintCallback()
↓
Manager 从受害者账户转账到 Pool(攻击者已授权)
↓
攻击者获得流动性份额,受害者代币被盗
三、修复方案
3.1 修复思路
采用双重验证机制:
- 验证调用者是合法的 Pool - 通过 Factory 的
getPool()映射验证 - 验证 player 是原始调用者 - 使用
tx.origin验证
3.2 修改的文件
| 文件 | 修改内容 |
|---|---|
src/UniswapV3Pool.sol | 添加 fee 状态变量,修改 CallbackData 结构 |
src/UniswapV3Factory.sol | 创建 Pool 时传入 fee 参数 |
src/UniswapV3Manager.sol | 添加 Factory 依赖和验证逻辑 |
3.3 关键代码修改
3.3.1 UniswapV3Pool.sol - 添加 fee 字段
// 添加 fee 状态变量
address public immutable token0;
address public immutable token1;
uint24 public immutable fee; // 新增
// 修改 CallbackData 结构
struct CallbackData {
address token0;
address token1;
address player;
uint24 fee; // 新增:用于验证 Pool 合法性
}
// 修改构造函数
constructor(
address token0_,
address token1_,
uint160 sqrtPriceX96_,
int24 tick_,
uint24 fee_ // 新增
) {
token0 = token0_;
token1 = token1_;
fee = fee_; // 新增
slot0 = Slot0({sqrtPriceX96: sqrtPriceX96_, tick: tick_});
}
3.3.2 UniswapV3Factory.sol - 传入 fee
function createPool(
address token0,
address token1,
uint24 fee
) external returns (address pool) {
// ... 省略验证代码 ...
// 部署新池子 - 传入 fee 参数
pool = address(new UniswapV3Pool(tokenA, tokenB, currentSqrtP, currentTick, fee));
// ... 省略后续代码 ...
}
3.3.3 UniswapV3Manager.sol - 核心修复
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "../src/UniswapV3Pool.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./UniswapV3Factory.sol";
contract UniswapV3Manager {
UniswapV3Factory public immutable factory;
// 新增:构造函数接收 Factory 地址
constructor(address factory_) {
factory = UniswapV3Factory(factory_);
}
/// @dev 验证调用者是 Factory 创建的合法池子
function _checkPool(address pool) internal view {
address token0 = UniswapV3Pool(pool).token0();
address token1 = UniswapV3Pool(pool).token1();
uint24 fee = UniswapV3Pool(pool).fee();
// 通过 Factory 查询:token0/token1/fee 对应的池子地址
address expectedPool = factory.getPool(token0, token1, fee);
require(pool == expectedPool, "Invalid pool");
}
function uniswapV3MintCallback(
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
_checkPool(msg.sender); // ✅ 验证1:调用者是合法 Pool
UniswapV3Pool.CallbackData memory extraData = abi.decode(
data,
(UniswapV3Pool.CallbackData)
);
require(extraData.player == tx.origin, "Invalid player"); // ✅ 验证2:player 是原始调用者
IERC20(extraData.token0).transferFrom(extraData.player, msg.sender, amount0);
IERC20(extraData.token1).transferFrom(extraData.player, msg.sender, amount1);
}
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata data
) external {
_checkPool(msg.sender); // ✅ 验证1:调用者是合法 Pool
UniswapV3Pool.CallbackData memory extraData = abi.decode(
data,
(UniswapV3Pool.CallbackData)
);
require(extraData.player == tx.origin, "Invalid player"); // ✅ 验证2:player 是原始调用者
if (amount0Delta > 0) {
IERC20(extraData.token0).transferFrom(extraData.player, msg.sender, uint256(amount0Delta));
}
if (amount1Delta > 0) {
IERC20(extraData.token1).transferFrom(extraData.player, msg.sender, uint256(amount1Delta));
}
}
}
3.4 安全机制说明
┌─────────────────────────────────────────────────────────────────┐
│ 安全验证流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 用户调用 Manager.mint() │
│ ↓ │
│ Manager 调用 Pool.mint() │
│ ↓ │
│ Pool 回调 Manager.uniswapV3MintCallback() │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ _checkPool(msg.sender) │ │
│ │ - msg.sender = Pool 地址 │ │
│ │ - 从 Pool 读取 token0, token1, fee │ │
│ │ - 查询 Factory.getPool(token0, token1, fee)│ │
│ │ - 验证 Pool 地址是否匹配 │ │
│ │ ✅ 防止恶意合约伪造回调 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ require(extraData.player == tx.origin) │ │
│ │ - tx.origin = 交易的原始发起者(EOA) │ │
│ │ - 无法被中间合约伪造 │ │
│ │ ✅ 防止伪造 player 地址 │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 执行 transferFrom(player, pool, amount) │
│ │
└─────────────────────────────────────────────────────────────────┘
四、验证结果
4.1 编译验证
$ forge build
[⠊] Compiling...
[⠒] Solc 0.8.30 finished in 895.79ms
Compiler run successful with warnings
4.2 测试验证
$ forge test --summary
╭-----------------------+--------+--------+---------╮
| Test Suite | Passed | Failed | Skipped |
+===================================================+
| DebugMintTest | 3 | 0 | 0 |
| DebugSwapTest | 2 | 0 | 0 |
| DebugSwapTest2 | 2 | 0 | 0 |
| PriceDirectionTest | 1 | 0 | 0 |
| UniswapV3PoolTest | 5 | 1 | 0 |
| MathFuzzTest | 7 | 0 | 0 |
| TickMathFuzzTest | 5 | 0 | 0 |
| UniswapV3PoolFuzzTest | 7 | 0 | 0 |
╰-----------------------+--------+--------+---------╯
32 tests passed, 1 pre-existing assertion failure (unrelated to security fix)
4.3 Slither 复检
修复后重新运行 Slither,arbitrary-send-erc20 漏洞已消除。
五、其他发现的高危问题
5.1 reentrancy-balance(重入攻击)
Reentrancy in UniswapV3Pool.mint():
External call: IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(...)
Balance read before call: balance0Before = balance0()
Possible stale balance used after call
状态: 已知设计模式(Uniswap V3 原版设计),通过 balance 检查机制防护。
5.2 unchecked-transfer(未检查转账返回值)
UniswapV3Pool.swap() ignores return value by IERC20(token1).transfer(recipient, amountOut)
状态: 需要后续修复,建议使用 safeTransfer 或检查返回值。
六、总结
修复成果
| 项目 | 修复前 | 修复后 |
|---|---|---|
| arbitrary-send-erc20 | ❌ 存在漏洞 | ✅ 已修复 |
| 编译状态 | - | ✅ 通过 |
| 测试状态 | - | ✅ 32/33 通过 |
最佳实践建议
- 回调函数验证: 所有 callback 函数应验证调用者身份
- 数据来源验证: 从
bytes data解码的地址必须验证其合法性 - 使用 tx.origin: 在特定场景下可用于验证原始调用者,但需注意其局限性(不支持合约钱包)
- 定期安全审计: 使用 Slither 等工具进行静态分析,及时发现潜在漏洞
详细报告可通过 slither . --output slither-report.json 生成。