从零构建 Uniswap V2 克隆:完整 Checklist 与现代化改进
目录
- 一、这篇文章的定位
- 二、语言与依赖的现代化选择
- 三、重入与安全
- 四、池子与储备管理
- 五、工厂合约的现代化
- 六、其他加固要点
- 七、测试与代码风格
- 八、总结
- 九、动手练习项目:你的完整 Uniswap V2 克隆(毕业项目)
一、这篇文章的定位
这是 Module 5 的收官篇,本质是一份实战 Checklist,指导你用现代 Solidity(或 Huff)从零重写 Uniswap V2。核心理念:
不要逐行抄代码,而要理解每个实现决策背后的原因。
原版 Uniswap V2 写于 2020 年的 Solidity 0.5/0.6 时代,很多写法在今天已经过时或有更优解。这篇清单告诉你哪些该保留、哪些该改进,是把前 8 篇知识落地的总纲。
二、语言与依赖的现代化选择
| 原版做法 | 现代化改进 | 原因 |
|---|---|---|
| Solidity 0.5/0.6 + SafeMath | Solidity 0.8+,去掉 SafeMath | 0.8+ 内置溢出检查,SafeMath 纯属冗余浪费 Gas |
| 内联 UQ112x112 库 | 用自定义类型(user-defined value type) 封装定点数 | 类型安全、可读性更好 |
| 自带 ERC20 实现 | 用 Solady 的 ERC20 | 极致 Gas 优化 |
| 自写 sqrt | 用 Solady 的 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 的精确顺序
burn、mint、_update 内部的操作顺序必须精确:先读余额、再计算、再转账/铸销、最后 _update 同步储备和累积器。顺序错了会导致 TWAP 累加用错价格、或留下重入窗口。
4.3 保留首铸销毁防御
首次注资销毁 MINIMUM_LIQUIDITY 的逻辑必须正确保留(mint/burn 篇详述),否则通胀攻击防御失效。这是最容易在”简化”时被误删的安全机制。
4.4 不要把 balance 当预言机
直接读 balanceOf 或现货储备比值做价格,会被闪电贷操纵。要做预言机就用 TWAP 累积器。克隆里务必保留并正确实现累积器逻辑。
五、工厂合约的现代化
- 现代 Solidity 不需要汇编也能做 CREATE2 部署,工厂可以大幅简化;
- 优化 pair 的追踪结构(
getPairmapping +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 的终极测试。关键清单:
- 现代 Solidity + Solady,去 SafeMath,但 TWAP 处用 unchecked;
- 用现代重入锁,警惕只读重入和内存膨胀;
- 取整永远偏向池子,操作顺序精确,保留首铸销毁;
- 不用现货价做预言机,用 TWAP;
- 工厂用 immutable + 简化 CREATE2;
- 自定义错误,全面单测 + 不变量测试。
把这份清单逐条打勾,你就拥有了一个安全、高效、属于自己的 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(逐条对应本文)
- Solidity 0.8+ 无 SafeMath,但 TWAP 累加放
unchecked; - Solady ERC20 + sqrt,sqrt 取整方向确认偏向池子;
- 现代重入锁(OZ ReentrancyGuard 或 transient storage 版),放在 swap/mint/burn;
- 首铸销毁 MINIMUM_LIQUIDITY,保留通胀攻击防御;
- 取整全部偏向池子:getAmountOut 向下、getAmountIn +1 向上、mint 取 min、burn 向下;
- TWAP 累积器正确实现,预言机不读现货;
- immutable 标注 factory/token0/token1;
- 全自定义错误,零字符串 require;
- 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 部署与端到端验证
- 写
Deploy.s.sol部署四件套 + WETH + 三种测试代币; - 建 A-B、B-C、WETH-A 三个池并注入流动性;
- 端到端脚本(或前端)依次执行:addLiquidity → swapExactTokensForTokens 多跳 → swapExactETHForTokens → 触发一次 mintFee 结算 → snapshot TWAP → 大额 swap 后对比 TWAP 与现货 → removeLiquidity;
- 全部合约在 Etherscan 验证源码;
- 用 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。