4. Uniswap V2 深度解析:核心合约架构、swap与addLiquidity流程、交易手续费分配机制、ERC20与ETH的配对处理

Uniswap V2 是我个人非常喜欢的一个合约设计。它简洁、优雅,而且把AMM的核心逻辑表达得特别清楚。我记得刚接触它的时候,花了整整一周去读源码,越读越觉得有意思。今天我们就来拆解它的核心模块。

4.1 核心合约架构

Uniswap V2 由三个核心合约组成:UniswapV2FactoryUniswapV2PairUniswapV2Router02。它们各司其职,配合得很默契。

合约 职责 我关注的点
Factory 创建和管理交易对 只允许创建一次,防止重复
Pair 持有流动性、执行swap 核心逻辑全在这里
Router 用户交互的入口 封装了安全检查和计算

Factory 合约里有个 createPair 函数,它会用 CREATE2 操作码来部署 Pair 合约。这样做的好处是——交易对地址可以提前算出来,不用等部署完成。我在项目中遇到过有人想用同一个代币对创建多个池子,结果被 Factory 的 require 拦住了,嗯,设计上就是故意的。

关键点:Pair 合约本身不存储路由逻辑,它只做两件事:管理储备金和执行交易。路由逻辑全部放在 Router 里,这样升级路由时不用动 Pair 合约。

4.2 swap 流程:从用户到合约

swap 的流程其实不复杂,但细节很多。我们一步步看。

  1. 用户调用 Router 的 swapExactTokensForTokens 或类似函数
  2. Router 计算输出数量,检查滑点
  3. Router 把用户代币转入 Pair 合约
  4. Pair 合约执行 swap 函数,更新储备金
  5. Pair 合约把输出代币转给用户

这里有个有意思的设计——闪电贷。Pair 合约的 swap 函数在转账之前会先调用一个回调函数 uniswapV2Call。这意味着你可以先借出代币,然后再还回去。我刚开始看的时候觉得这太疯狂了,后来发现这其实是 V2 最巧妙的设计之一。

我的经验:写 Router 合约时,一定要在 swap 前后检查储备金的变化。我见过有人因为忘记检查 kLast 而导致手续费计算错误。说白了,就是少了一行 require

4.3 addLiquidity 流程:如何添加流动性

添加流动性比 swap 稍微复杂一点,因为涉及到价格匹配的问题。

流程是这样的:

  • 用户指定两个代币的数量
  • Router 根据当前储备金计算最优比例
  • 如果用户给的比例不对,Router 会按比例调整
  • 代币转入 Pair 合约,铸造 LP Token
  • LP Token 转给用户

你想想看,如果用户想添加 100 USDC 和 1 ETH,但当前池子里是 1000 USDC 和 10 ETH,比例是 100:1。用户给的比例也是 100:1,那就刚好。但如果用户给了 100 USDC 和 2 ETH,Router 只会收走 100 USDC 和 1 ETH,剩下的 1 ETH 退回去。

注意:添加流动性时,Router 会先检查用户是否授权了足够的代币。我建议你在前端就做好授权检查,不然用户会看到莫名其妙的 revert 错误。我曾经因为这个被用户骂过...

4.4 交易手续费分配机制

Uniswap V2 的手续费是 0.3%,全部归流动性提供者。这个费用是在每次 swap 时自动收取的。

具体怎么收?

每次 swap 完成后,Pair 合约会更新储备金。但注意,它不会直接把 0.3% 的输入代币扣下来。而是通过一个公式:k = x * y,其中 k 在每次交易后会略微增加。这个增加的部分就是手续费。

举个例子:

  • 交易前:x=100, y=100, k=10000
  • 用户输入 1 个 x,按 0.3% 费率,实际进入池子的是 0.997 个 x
  • 计算输出:y' = 100 * 100 / (100 + 0.997) ≈ 99.01
  • 用户得到 100 - 99.01 = 0.99 个 y
  • 交易后:x=100.997, y=99.01, k≈10000.7

看到没?k 从 10000 变成了 10000.7,这 0.7 就是手续费积累。流动性提供者提取流动性时,就能拿到这部分收益。

核心逻辑:手续费不是单独收取的,而是通过 k 值的增长来体现。这种设计让合约代码非常简洁,但理解起来需要一点数学基础。

4.5 ERC20 与 ETH 的配对处理

这里有个坑——Uniswap V2 的 Pair 合约只支持 ERC20 代币。ETH 本身不是 ERC20,那怎么处理 ETH 交易对呢?

答案是:WETH

Router 合约会把用户的 ETH 自动包装成 WETH,然后再跟其他 ERC20 配对。流程如下:

  • 用户发送 ETH 到 Router
  • Router 调用 WETH 合约的 deposit 函数,把 ETH 变成 WETH
  • WETH 再进入 Pair 合约
  • 交易完成后,Router 把 WETH 转回 ETH 给用户

我建议你在写前端交互时,一定要区分用户输入的是 ETH 还是 ERC20。因为 ETH 的转账逻辑跟 ERC20 完全不一样。ERC20 需要先 approve,ETH 直接发送就行。搞混了的话,用户会看到 "transfer failed" 的错误。

避坑指南:我曾经在测试网上调试一个 ETH/DAI 交易对,发现 swap 总是失败。查了半天,原来是 Router 合约的 msg.value 没有正确传递。记住,ETH 交易对必须用 payable 函数,并且要检查 msg.value 是否等于用户输入的 ETH 数量。

4.6 核心流程图

下面这张图展示了 Uniswap V2 的核心交互流程,从用户到 Router 再到 Pair 合约。我画的时候特意把 WETH 的包装过程也加进去了,因为这是最容易出错的地方。

用户 Router WETH 合约 Pair 合约 Factory 1. 发送交易 2. 包装 ETH 3. 转入代币 4. 查询储备 5. 返回数据 6. 输出代币 7. 返回结果 图例说明 用户:发起交易 Router:路由合约,处理逻辑 WETH:包装 ETH 为 ERC20 Pair:交易对合约,持有流动性 Factory:工厂合约,创建 Pair

这张图里,我特意把 WETH 的包装过程单独画出来了。因为在实际开发中,很多人会忘记处理 ETH 到 WETH 的转换。记住,Pair 合约只认 ERC20,ETH 必须走 WETH 这条路。

4.7 总结

Uniswap V2 的设计思路很清晰:Factory 负责创建,Pair 负责核心逻辑,Router 负责用户交互。三者配合,形成了一个完整的去中心化交易所。

我个人觉得,V2 最值得学习的地方是它的简洁性。整个合约代码不到 500 行,却实现了 AMM、闪电贷、手续费分配等核心功能。你想想看,现在很多 DeFi 项目动辄几千行代码,但核心逻辑其实跟 V2 差不多。

如果你要自己写 AMM 合约,我建议你先把 V2 的源码读透。别急着抄代码,先理解它的设计哲学。嗯,就这样。


专注资料整理