返回博客列表

Uniswap V3 Clone 安全审计报告

web3
安全审计SlitherSolidityUniswap V3

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

Uniswap V3 Clone 安全审计报告

合约代码写完跑通了,心里踏实了?别急,安全审计这关迟早得过。这次我用 Slither 对自己的 Uniswap V3 Clone 项目做了一轮静态分析,结果嘛……说实话,看到报告的那一刻还是有点冒冷汗的。

这篇记录整个排查和修复过程,给同样在写 DeFi 合约的朋友留个参考。


审计概况

跑完 Slither 之后,一共报了 62 个问题,其中高危的有 3 个。最要命的是一个叫 arbitrary-send-erc20 的漏洞,简单说就是攻击者可以伪造回调数据,把你的 token 从别的用户账户里转走**。

听起来很严重对吧?我们一个一个来看。


最危险的漏洞:arbitrary-send-erc20

问题出在哪

漏洞在 UniswapV3Manager 的两个回调函数里:uniswapV3MintCallbackuniswapV3SwapCallback

问题出在这行代码:

IERC20(extraData.token0).transferFrom(extraData.player, msg.sender, amount0);

这里的 extraData.player 是从 bytes data 参数里解码出来的——而这个 data 参数是调用者随便传的。也就是说,攻击者可以把自己的地址换成受害者的地址,然后 Manager 就会尝试从受害者账户里转 token。

如果受害者恰好授权过攻击者(比如一些钓鱼场景),那 token 就真的会被转走。

攻击流程

整个过程大概是这样:

攻击者调用 Manager.mint(),传入伪造的 data:
  ↓
data 里的 player 字段被设成受害者地址
  ↓
Pool 回调 Manager.uniswapV3MintCallback()
  ↓
Manager 从受害者账户转账到 Pool(前提是受害者授权过攻击者)
  ↓
攻击者拿到流动性份额,受害者 token 被盗

Slither 的检测输出

Detector: arbitrary-send-erc20
UniswapV3Manager.uniswapV3MintCallback(uint256,uint256,bytes) uses arbitrary from in transferFrom:
    IERC20(extraData.token0).transferFrom(extraData.player,msg.sender,amount0)
UniswapV3Manager.uniswapV3SwapCallback(int256,int256,bytes) uses arbitrary from in transferFrom:
    IERC20(extraData.token1).transferFrom(extraData.player,msg.sender,uint256(amount1Delta))

Slither 直接指出了两行有问题的代码,很精准。


修复过程

思路

修这个漏洞需要解决两个问题:

  1. 确保回调的 Pool 是合法的——不是随便哪个合约都能调用 Manager 的回调函数
  2. 确保 player 是原始调用者——不能让调用者随便伪造一个地址

具体改动

改动了三个文件:

文件改动
UniswapV3Pool.sol添加 fee 状态变量,修改 CallbackData 结构
UniswapV3Factory.sol创建 Pool 时传入 fee 参数
UniswapV3Manager.sol添加 Factory 依赖,加上验证逻辑

核心修复代码

Manager 里加了个 _checkPool 函数,用来验证调用者是不是 Factory 创建的正经 Pool:

function _checkPool(address pool) internal view {
    address token0 = UniswapV3Pool(pool).token0();
    address token1 = UniswapV3Pool(pool).token1();
    uint24 fee = UniswapV3Pool(pool).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);  // 第一层:确认调用者是合法 Pool

    UniswapV3Pool.CallbackData memory extraData = abi.decode(
        data,
        (UniswapV3Pool.CallbackData)
    );

    require(extraData.player == tx.origin, "Invalid player");  // 第二层:player 必须是交易原始发起者

    IERC20(extraData.token0).transferFrom(extraData.player, msg.sender, amount0);
    IERC20(extraData.token1).transferFrom(extraData.player, msg.sender, amount1);
}

swapCallback 也做了同样的处理。

为什么用 tx.origin

这里用了 tx.origin 来验证原始调用者。说实话,tx.origin 在 Solidity 社区争议挺大的,因为它不支持合约钱包(中间有任何合约调用的话 tx.origin 就不是最终调用者了)。

但在这个场景下反而是合适的——因为 Mint 和 Swap 操作本来就应该是由 EOA(普通用户钱包)直接发起的,中间不应该有其他合约代理。这样既防住了伪造 player 的攻击,也不会影响正常使用。

当然,如果后续要支持合约钱包,这个逻辑需要改成更完善的方案,比如通过 Manager 记录每次调用的 caller 映射。

安全验证流程

修完之后的验证链是这样的:

用户调用 Manager.mint()
         ↓
Manager 调用 Pool.mint()
         ↓
Pool 回调 Manager.uniswapV3MintCallback()
         ↓
┌─────────────────────────────────┐
│ _checkPool(msg.sender)          │
│   从 Pool 读取 token0/token1/fee│
│   查 Factory.getPool() 对比地址  │
│   ✅ 防止恶意合约伪造回调        │
└─────────────────────────────────┘
         ↓
┌─────────────────────────────────┐
│ require(player == tx.origin)     │
│   验证 player 是交易原始发起者   │
│   ✅ 防止伪造 player 地址        │
└─────────────────────────────────┘
         ↓
执行 transferFrom,安全 ✅

验证结果

编译和测试

改完后先跑编译:

$ forge build
Compiler run successful with warnings

然后跑测试:

$ forge test --summary

32 tests passed, 1 pre-existing assertion failure (unrelated to security fix)

之前通过的测试都还是通过的,说明修复没有破坏现有功能。

Slither 复检

最关键的一步——重新跑 Slither,确认漏洞消除了:

$ slither .
# arbitrary-send-erc20 不再出现在报告中 ✅

其他发现

除了这个高危漏洞,Slither 还报了一些其他问题,简单记录一下:

reentrancy-balance(重入攻击)

Slither 报了 mint() 函数的重入风险。这个其实是 Uniswap V3 原版的设计模式——通过比较回调前后的 balance 来检测重入,属于已知设计,不是 bug。

unchecked-transfer(未检查转账返回值)

UniswapV3Pool.swap() ignores return value by IERC20(token1).transfer(recipient, amountOut)

这个确实需要注意。有些 token 的 transfer 返回值是 false 表示失败,但代码里没检查。后续应该换成 safeTransfer 或者手动检查返回值。


总结

项目修复前修复后
arbitrary-send-erc20❌ 存在✅ 已修复
编译-✅ 通过
测试-✅ 32/33 通过

几条经验教训

  1. 回调函数永远不要信任 bytes data 里的地址——一定要验证来源
  2. Pool 合法性要验证——通过 Factory 反查是最靠谱的方式
  3. tx.origin 有局限但特定场景够用——如果未来支持合约钱包得换方案
  4. 定期跑 Slither——别等上线了才想起来审计,开发过程中就该定期扫

详细报告可以用 slither . --output slither-report.json 生成 JSON 版本。


安全是 DeFi 的生命线,宁可多花时间,也不能带病上线。