返回博客列表

Uniswap V3 Clone 安全审计报告

web3
安全审计SlitherSolidityUniswap V3

⚠️ 免责声明:本文内容仅供技术学习与研究参考,不构成任何投资建议。请读者独立思考,谨慎决策。

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 修复思路

采用双重验证机制:

  1. 验证调用者是合法的 Pool - 通过 Factory 的 getPool() 映射验证
  2. 验证 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 通过

最佳实践建议

  1. 回调函数验证: 所有 callback 函数应验证调用者身份
  2. 数据来源验证: 从 bytes data 解码的地址必须验证其合法性
  3. 使用 tx.origin: 在特定场景下可用于验证原始调用者,但需注意其局限性(不支持合约钱包)
  4. 定期安全审计: 使用 Slither 等工具进行静态分析,及时发现潜在漏洞

详细报告可通过 slither . --output slither-report.json 生成。