从零构建 Uniswap V2 克隆:完整 Checklist 与现代化改进

用现代 Solidity 重写 Uniswap V2 的实战清单:安全、取整方向、重入防护与 Gas 优化要点

8 分钟阅读
从零构建 Uniswap V2 克隆:完整 Checklist 与现代化改进

从零构建 Uniswap V2 克隆:完整 Checklist 与现代化改进

目录


一、这篇文章的定位

这是 Module 5 的收官篇,本质是一份实战 Checklist,指导你用现代 Solidity(或 Huff)从零重写 Uniswap V2。核心理念:

不要逐行抄代码,而要理解每个实现决策背后的原因

原版 Uniswap V2 写于 2020 年的 Solidity 0.5/0.6 时代,很多写法在今天已经过时或有更优解。这篇清单告诉你哪些该保留、哪些该改进,是把前 8 篇知识落地的总纲。

二、语言与依赖的现代化选择

原版做法现代化改进原因
Solidity 0.5/0.6 + SafeMathSolidity 0.8+,去掉 SafeMath0.8+ 内置溢出检查,SafeMath 纯属冗余浪费 Gas
内联 UQ112x112 库自定义类型(user-defined value type) 封装定点数类型安全、可读性更好
自带 ERC20 实现Solady 的 ERC20极致 Gas 优化
自写 sqrtSolady 的 sqrt(注意取整方向)经过优化和审计

⚠️ 但有一个反向注意点:TWAP 累积器和时间戳的溢出是故意依赖回绕的,在 0.8+ 里必须用 unchecked 包起来,否则会被默认溢出检查 revert(详见 TWAP 篇)。

三、重入与安全

3.1 不要照抄 V2 的重入锁

“不要用 Uniswap V2 现在的重入保护,它已经不再 Gas 高效了。”

原版用一个 uint private unlocked = 1 配 modifier 切换。今天更推荐用 OpenZeppelin 的 ReentrancyGuard(或 Solady 版本),它们用 transient storage(EIP-1153)等新特性做得更省 Gas。

而且要把锁放在正确的位置——swap、mint、burn 这些会对外转账/回调的函数都需要,但别无脑全加(view 函数加锁反而可能引入问题,见下)。

3.2 只读重入

这是 V2 时代还不为人熟知、后来酿成多起事故的漏洞:只读重入(read-only reentrancy)

攻击者在回调中间状态(比如 swap 已转出代币但储备还没更新)调用你的 getReserves 之类的 view 函数,此时读到的是不一致的中间状态。如果别的协议信任这个读数,就会被欺骗。设计克隆时必须考虑 view 函数在重入窗口内返回的值是否安全。

3.3 转账中的内存膨胀攻击

实现 _safeTransfer 处理代币返回值时,要用定向的 returndatacopy 而非无脑 copy 全部返回数据。否则恶意代币可以返回超大 returndata,撑爆内存让你的交易因 Gas 耗尽而失败(内存膨胀攻击)。

四、池子与储备管理

4.1 永远朝有利于池子的方向取整

“做交易、铸造或销毁份额时,永远朝有利于池子的方向取整。”

整数除法必然产生舍入。舍入产生的微小误差必须留给池子(LP),而不是给套利者。比如算用户输出时向下取整,算用户应付输入时向上取整(getAmountIn 末尾 +1 就是这个原则)。一旦取整方向反了,攻击者可以反复利用微小优势”薅”出池子的钱(rounding attack)。

4.2 burn/mint/update 的精确顺序

burnmint_update 内部的操作顺序必须精确:先读余额、再计算、再转账/铸销、最后 _update 同步储备和累积器。顺序错了会导致 TWAP 累加用错价格、或留下重入窗口。

4.3 保留首铸销毁防御

首次注资销毁 MINIMUM_LIQUIDITY 的逻辑必须正确保留(mint/burn 篇详述),否则通胀攻击防御失效。这是最容易在”简化”时被误删的安全机制。

4.4 不要把 balance 当预言机

直接读 balanceOf 或现货储备比值做价格,会被闪电贷操纵。要做预言机就用 TWAP 累积器。克隆里务必保留并正确实现累积器逻辑。

五、工厂合约的现代化

  • 现代 Solidity 不需要汇编也能做 CREATE2 部署,工厂可以大幅简化;
  • 优化 pair 的追踪结构(getPair mapping + allPairs 数组)的 Gas;
  • 把合适的存储变量标成 immutable(如 factory 地址、两个 token 地址)——immutable 直接嵌进字节码,读取不花存储 Gas。

六、其他加固要点

  • 抵御转账收费 / rebasing 代币:这类代币会让”转入多少 = 到账多少”的假设失效,要么用实测余额、要么明确不支持并文档化;
  • 全面用自定义错误替代 require(条件, "字符串"),省部署和运行 Gas;
  • 若把 Router 功能集成进来,记得实现滑点保护
  • 用 Solady 优化版 sqrt,并确认取整方向符合 4.1。

七、测试与代码风格

  • 全面的单元测试覆盖每个函数和边界;
  • 遵循专业 Solidity 风格规范(命名、NatSpec 注释、函数排序);
  • 重点写不变量测试(invariant testing):最核心的不变量是”在没有 mint/burn 的情况下,流动性(√K)只增不减”——用 Foundry 的 invariant 测试框架反复随机调用 swap,断言它永远成立;
  • 初版跑通后,再回头读 Gas 优化资料逐项压榨。

八、总结

构建克隆是检验你是否真懂 Uniswap V2 的终极测试。关键清单:

  1. 现代 Solidity + Solady,去 SafeMath,但 TWAP 处用 unchecked;
  2. 用现代重入锁,警惕只读重入和内存膨胀;
  3. 取整永远偏向池子,操作顺序精确,保留首铸销毁;
  4. 不用现货价做预言机,用 TWAP;
  5. 工厂用 immutable + 简化 CREATE2;
  6. 自定义错误,全面单测 + 不变量测试。

把这份清单逐条打勾,你就拥有了一个安全、高效、属于自己的 DEX。


九、动手练习项目:你的完整 Uniswap V2 克隆(毕业项目)

这是整个 Module 5 的毕业项目——把前 8 篇练习中分散实现的组件,整合成一个生产级质量的完整 DEX。

项目目标

从零实现一个名为 MySwap 的完整 Uniswap V2 克隆,包含 Factory + Pair + Library + Router 四件套,应用本文全部 Checklist,通过全面单测 + 不变量测试,部署到 Sepolia 并跑通端到端流程。

合约结构要求

src/
  MySwapFactory.sol     // CREATE2 部署 Pair,immutable 优化,getPair + allPairs
  MySwapPair.sol        // 继承 Solady ERC20,含 swap/mint/burn/_update/TWAP 累积器
  MySwapLibrary.sol     // sortTokens/pairFor/getReserves/quote/getAmountOut/In/getAmountsOut/In
  MySwapRouter.sol      // 滑点保护 + deadline + 多跳 + ETH + addLiquidity/removeLiquidity
  libraries/
    UQ112x112.sol       // 用 user-defined value type 实现定点数
    SafeTransfer.sol     // _safeTransfer,用定向 returndatacopy 防内存膨胀
test/
  Pair.t.sol  Library.t.sol  Router.t.sol  Invariant.t.sol

必须落实的 Checklist(逐条对应本文)

  1. Solidity 0.8+ 无 SafeMath,但 TWAP 累加放 unchecked
  2. Solady ERC20 + sqrt,sqrt 取整方向确认偏向池子;
  3. 现代重入锁(OZ ReentrancyGuard 或 transient storage 版),放在 swap/mint/burn;
  4. 首铸销毁 MINIMUM_LIQUIDITY,保留通胀攻击防御;
  5. 取整全部偏向池子:getAmountOut 向下、getAmountIn +1 向上、mint 取 min、burn 向下;
  6. TWAP 累积器正确实现,预言机不读现货;
  7. immutable 标注 factory/token0/token1;
  8. 全自定义错误,零字符串 require;
  9. Router 含 deadline(外部传入)+ 滑点保护

测试要求(Foundry,分层)

单元测试:复用前 8 篇练习的所有测试用例(swap K 校验、mint sqrt/min、burn 按比例、mintFee 1/6、TWAP 抗操纵、Library 公式、Router 滑点/deadline)。

不变量测试(本项目新增重点):

  • invariant_KNeverDecreasesWithoutBurn:随机序列地 swap/mint(不 burn),断言 √K 单调不减;
  • invariant_LpTokenSolvency:任意时刻,所有 LP 份额按比例可赎回的资产 ≤ 池子实际余额(池子永不资不抵债);
  • invariant_ReserveMatchesBalance:每次操作后 reserve 与真实 balance 一致(除非有人直接捐赠);
  • invariant_RoundingFavorsPool:构造大量微小 swap,断言池子的 √K 不会因取整被持续薅损。

攻击测试(安全实验):

  • test_RoundingAttack_Fails:尝试用极小额反复交易薅池子,断言无法获利;
  • test_ReadOnlyReentrancy:构造一个在回调里读 getReserves 的恶意合约,验证读数安全或被锁阻止;
  • test_InflationAttack_Mitigated:复现通胀攻击,验证 MINIMUM_LIQUIDITY 防御。

Sepolia 部署与端到端验证

  1. Deploy.s.sol 部署四件套 + WETH + 三种测试代币;
  2. 建 A-B、B-C、WETH-A 三个池并注入流动性;
  3. 端到端脚本(或前端)依次执行:addLiquidity → swapExactTokensForTokens 多跳 → swapExactETHForTokens → 触发一次 mintFee 结算 → snapshot TWAP → 大额 swap 后对比 TWAP 与现货 → removeLiquidity;
  4. 全部合约在 Etherscan 验证源码;
  5. 用 Foundry 的 forge snapshot 生成 Gas 报告,对照原版 Uniswap V2 看你优化了多少。

进阶挑战(真正的高手向)

  • Huff 重写 Pair 的 swap 热路径,对比 Gas;
  • 接入 DamnVulnerableDeFi 的 Puppet V2 关卡,用你的克隆复现”误用 quote 现货价做预言机”导致的清算攻击,再用 TWAP 修复它——这会让你彻底理解为什么现货价不能做预言机;
  • 给你的克隆补一篇完整的 NatSpec 文档和 README 架构图,当作你的 DeFi 作品集项目。

至此,RareSkills Module 5「Uniswap V2 Walkthrough」全部 9 篇学习文档完成。建议按顺序完成每篇的练习项目,最后用本篇的毕业项目把它们串成一个完整的、署你名字的 DEX。

💬 评论