Uniswap V2 深度解析:从代码到机制

  • 乡客
  • 发布于 1天前
  • 阅读 103

目录概述核心架构核心机制关键合约解析安全特性价格预言机个人见解与学习概述UniswapV2是一个去中心化的自动做市商(AMM)协议,允许用户在以太坊上交换ERC20代币,而无需传统的订单簿。它通过恒定乘积公式x*y=k来维持流动性池的平衡。核心特点无

目录

  1. 概述
  2. 核心架构
  3. 核心机制
  4. 关键合约解析
  5. 安全特性
  6. 价格预言机
  7. 个人见解与学习

概述

Uniswap V2 是一个去中心化的自动做市商(AMM)协议,允许用户在以太坊上交换 ERC20 代币,而无需传统的订单簿。它通过恒定乘积公式 x * y = k 来维持流动性池的平衡。

核心特点

  • 无需许可:任何人都可以创建交易对和添加流动性
  • 去中心化:完全在链上运行,无需中心化服务器
  • 自动化:通过数学公式自动定价,无需人工干预
  • 可组合性:可以与其他 DeFi 协议组合使用

核心架构

Uniswap V2 由两个主要部分组成:

1. 核心合约(Core)

  • UniswapV2Factory: 工厂合约,负责创建和管理交易对
  • UniswapV2Pair: 交易对合约,实现 AMM 的核心逻辑

2. 周边合约(Periphery)

  • UniswapV2Router02: 路由器合约,提供用户友好的接口
  • UniswapV2Library: 工具库,包含计算函数和地址计算

数据流

用户 -> Router -> Factory -> Pair
                -> Library (计算)

核心机制

1. 恒定乘积公式(Constant Product Formula)

Uniswap V2 使用恒定乘积公式来维持流动性池的平衡:

reserve0 * reserve1 = k (常数)

变量说明

  • reserve0: 交易对中第一种代币(token0)的储备量(单位:代币的最小单位,如 wei)
  • reserve1: 交易对中第二种代币(token1)的储备量(单位:代币的最小单位,如 wei)
  • k: 恒定乘积常数,表示流动性池的总价值(在扣除手续费前保持不变)

工作原理

  • 当用户交换代币时,公式确保 k 值保持不变(扣除手续费后)
  • 这意味着储备量越多,价格影响越小
  • 公式自动计算交换价格,无需外部价格源

示例: 假设池中有 1000 TokenA 和 1000 TokenB,k = 1,000,000

  • 用户想用 100 TokenA 换 TokenB
  • 新储备:1100 TokenA,需要保持 k = 1,000,000
  • 新 TokenB 储备 = 1,000,000 / 1100 ≈ 909.09
  • 用户得到:1000 - 909.09 = 90.91 TokenB

2. 流动性提供机制

添加流动性(Mint)

当用户首次添加流动性时:

liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY

变量说明

  • liquidity: 应铸造给用户的 LP token 数量(单位:LP token 的最小单位)
  • amount0: 用户添加的第一种代币(token0)的数量(单位:代币的最小单位)
  • amount1: 用户添加的第二种代币(token1)的数量(单位:代币的最小单位)
  • MINIMUM_LIQUIDITY: 最小流动性常量,固定为 1000 wei,用于防止首次添加时被全部提取

后续添加时:

liquidity = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1)

变量说明

  • liquidity: 应铸造给用户的 LP token 数量(单位:LP token 的最小单位)
  • amount0: 用户添加的第一种代币(token0)的数量(单位:代币的最小单位)
  • amount1: 用户添加的第二种代币(token1)的数量(单位:代币的最小单位)
  • totalSupply: 当前 LP token 的总供应量(单位:LP token 的最小单位)
  • reserve0: 交易对中第一种代币(token0)的当前储备量(单位:代币的最小单位)
  • reserve1: 交易对中第二种代币(token1)的当前储备量(单位:代币的最小单位)
  • min(): 取两个值中的较小值,确保按比例添加流动性

关键点

  • 首次添加时,会永久锁定 MINIMUM_LIQUIDITY(1000 wei)的 LP token
  • 这防止了首次添加时被全部提取,避免除零错误
  • 后续添加必须按比例,取两个代币中较小的流动性值

移除流动性(Burn)

amount0 = liquidity * balance0 / totalSupply
amount1 = liquidity * balance1 / totalSupply

变量说明

  • amount0: 用户将获得的第一种代币(token0)的数量(单位:代币的最小单位)
  • amount1: 用户将获得的第二种代币(token1)的数量(单位:代币的最小单位)
  • liquidity: 用户要销毁的 LP token 数量(单位:LP token 的最小单位)
  • balance0: 交易对合约中第一种代币(token0)的当前余额(单位:代币的最小单位)
  • balance1: 交易对合约中第二种代币(token1)的当前余额(单位:代币的最小单位)
  • totalSupply: 当前 LP token 的总供应量(单位:LP token 的最小单位)

工作原理

  • 用户按比例获得两种代币,比例等于其 LP token 占总供应量的比例
  • 例如:如果用户持有 10% 的 LP token,将获得池中 10% 的两种代币

3. 交换机制

手续费

  • 每次交换收取 0.3% 的手续费
  • 手续费留在池中,增加流动性提供者的收益

交换公式

输出数量计算(考虑 0.3% 手续费):

amountInWithFee = amountIn * 997
numerator = amountInWithFee * reserveOut
denominator = reserveIn * 1000 + amountInWithFee
amountOut = numerator / denominator

变量说明

  • amountIn: 用户输入的代币数量(单位:代币的最小单位)
  • amountInWithFee: 扣除手续费后的输入数量,997 = 1000 - 3(0.3% 手续费 = 3/1000)
  • reserveOut: 输出代币的当前储备量(单位:代币的最小单位)
  • reserveIn: 输入代币的当前储备量(单位:代币的最小单位)
  • numerator: 分子,用于计算输出数量
  • denominator: 分母,用于计算输出数量
  • amountOut: 用户将获得的输出代币数量(单位:代币的最小单位)

验证(确保 k 值不减少):

(balance0 * 1000 - amount0In * 3) * (balance1 * 1000 - amount1In * 3) >= reserve0 * reserve1 * 1000^2

变量说明

  • balance0: 交换后第一种代币(token0)的余额(单位:代币的最小单位)
  • balance1: 交换后第二种代币(token1)的余额(单位:代币的最小单位)
  • amount0In: 第一种代币的输入数量(如果用户输入的是 token0,否则为 0)(单位:代币的最小单位)
  • amount1In: 第二种代币的输入数量(如果用户输入的是 token1,否则为 0)(单位:代币的最小单位)
  • reserve0: 交换前第一种代币(token0)的储备量(单位:代币的最小单位)
  • reserve1: 交换前第二种代币(token1)的储备量(单位:代币的最小单位)
  • 1000: 用于处理 0.3% 手续费(3/1000),乘以 1000 避免小数运算
  • 3: 手续费部分(0.3% = 3/1000)

工作原理

  • 公式确保交换后的恒定乘积(扣除手续费后)不小于交换前的恒定乘积
  • 这保证了池子不会被耗尽,并且流动性提供者能够获得手续费收益

4. 协议费用(可选)

如果启用了协议费用(通过 Factory 的 feeTo 设置):

  • 协议收取交易手续费的 1/6(即总手续费的 16.67%)
  • 费用通过铸造 LP token 的方式收取,而不是直接扣除代币
  • 费用公式:liquidity = totalSupply * (sqrt(k) - sqrt(kLast)) / (5 * sqrt(k) + sqrt(kLast))

变量说明

  • liquidity: 应铸造给协议费用接收地址的 LP token 数量(单位:LP token 的最小单位)
  • totalSupply: 当前 LP token 的总供应量(单位:LP token 的最小单位)
  • k: 当前恒定乘积值,k = reserve0 * reserve1(单位:代币数量的平方)
  • kLast: 上次更新储备量时的恒定乘积值(单位:代币数量的平方)
  • sqrt(): 平方根函数,用于计算流动性增长
  • 5: 协议费用比例系数,表示协议收取总手续费的 1/6(即 1/(1+5) = 1/6)

工作原理

  • k 值增加时(由于手续费累积),计算应铸造给协议的 LP token
  • 公式确保协议获得手续费收益的 1/6,其余 5/6 归流动性提供者所有
  • 通过铸造 LP token 而非直接扣除代币,保持了池子的完整性

关键合约解析

UniswapV2Factory

职责:创建和管理交易对

核心函数

  1. createPair

    • 使用 CREATE2 确定性部署创建交易对
    • 确保相同参数下总是创建相同地址的交易对
    • 使用 keccak256(abi.encodePacked(token0, token1)) 作为 salt
  2. getPair

    • 快速查找两个代币之间的交易对地址
    • 使用映射:token0 => token1 => pairAddress

设计亮点

  • CREATE2 确保地址确定性,便于前端集成
  • 双向映射(token0->token1 和 token1->token0)方便查询

UniswapV2Pair

职责:实现 AMM 的核心逻辑

核心函数

  1. mint(添加流动性)

    步骤:
    1. 获取当前储备量和余额
    2. 计算新增的代币数量
    3. 处理协议费用(如果启用)
    4. 计算应铸造的 LP token 数量
    5. 如果是首次,锁定最小流动性
    6. 铸造 LP token
    7. 更新储备量
  2. burn(移除流动性)

    步骤:
    1. 获取当前储备量
    2. 计算应返回的代币数量(按比例)
    3. 销毁 LP token
    4. 转移代币给用户
    5. 更新储备量
  3. swap(交换)

    步骤:
    1. 验证输出数量
    2. 先转移输出代币(乐观转移)
    3. 如果提供回调,执行回调(闪电贷)
    4. 获取新余额,计算输入数量
    5. 验证恒定乘积公式(扣除手续费后)
    6. 更新储备量
  4. _update(更新储备量)

    • 更新储备量和时间戳
    • 如果是区块内首次更新,更新价格累积值(用于价格预言机)

设计亮点

  • 使用 uint112 存储储备量,三个变量打包在一个存储槽中(节省 gas)
  • 乐观转移模式:先转移,后验证
  • 重入锁保护:防止重入攻击

UniswapV2Router02

职责:提供用户友好的接口

核心功能

  1. 添加流动性

    • addLiquidity: ERC20/ERC20 交易对
    • addLiquidityETH: ERC20/ETH 交易对(通过 WETH)
  2. 移除流动性

    • removeLiquidity: 移除 ERC20/ERC20 流动性
    • removeLiquidityETH: 移除 ERC20/ETH 流动性
    • 支持 Permit 签名,无需预先授权
  3. 交换

    • swapExactTokensForTokens: 精确输入,交换代币
    • swapTokensForExactTokens: 精确输出,交换代币
    • 支持 ETH 交换(通过 WETH)
    • 支持多跳交换(通过多个交易对)
  4. 税费代币支持

    • 特殊函数处理在转账时收取费用的代币
    • 通过比较交换前后的余额来验证输出数量

设计亮点

  • 使用 pairFor 函数计算交易对地址(需要正确的 init_code_hash
  • 所有 ETH 操作都通过 WETH 处理,保持一致性
  • 支持滑点保护(通过 amountMin 参数)

init_code_hash 的重要性

init_code_hash 是 UniswapV2Pair 合约创建字节码的 keccak256 哈希值,用于在 pairFor 函数中计算交易对的 CREATE2 地址。

为什么重要

  • 如果 init_code_hash 不正确,pairFor 计算的地址会错误
  • Router 会尝试向错误的地址发送代币,导致交易失败
  • 每次部署新的 Pair 合约时,如果字节码不同,init_code_hash 也会不同

如何计算

bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 initCodeHash = keccak256(bytecode);

如何更新: 在 UniswapV2Library.solpairFor 函数中更新硬编码的哈希值。


安全特性

1. 重入保护

  • 使用 lock 修饰符防止重入攻击
  • 在关键函数执行期间锁定合约

2. 溢出保护

  • 使用 SafeMath 库进行安全的数学运算
  • 储备量使用 uint112,防止溢出

3. 滑点保护

  • Router 提供 amountMin 参数
  • 用户设置最小输出数量,防止价格滑点过大

4. 时间保护

  • 使用 deadline 参数
  • 防止交易在链上拥堵时执行过期的交易

5. 恒定乘积验证

  • 每次交换后验证 k 值不减少
  • 确保池子不会被耗尽

价格预言机

Uniswap V2 内置了价格预言机功能,通过累积价格(Cumulative Price)实现。

工作原理

每个区块首次交换时,更新累积价格:

price0CumulativeLast += (reserve1 / reserve0) * timeElapsed
price1CumulativeLast += (reserve0 / reserve1) * timeElapsed

变量说明

  • price0CumulativeLast: token0 相对于 token1 的累积价格(单位:价格 * 秒数,无单位)
  • price1CumulativeLast: token1 相对于 token0 的累积价格(单位:价格 * 秒数,无单位)
  • reserve0: 第一种代币(token0)的储备量(单位:代币的最小单位)
  • reserve1: 第二种代币(token1)的储备量(单位:代币的最小单位)
  • timeElapsed: 自上次更新以来经过的时间(单位:秒)
  • +=: 累加操作,将当前价格乘以时间间隔累加到累积价格中

工作原理

  • 每个区块首次交换时,计算当前价格(储备量比例)并乘以时间间隔
  • 累积价格持续累加,形成价格历史记录
  • 通过这种方式,可以计算任意时间段的平均价格

计算时间加权平均价格(TWAP)

TWAP = (priceCumulativeEnd - priceCumulativeStart) / timeElapsed

变量说明

  • TWAP: 时间加权平均价格(Time-Weighted Average Price)(单位:价格,如 token1/token0 或 token0/token1)
  • priceCumulativeEnd: 结束时间点的累积价格(单位:价格 * 秒数,无单位)
  • priceCumulativeStart: 开始时间点的累积价格(单位:价格 * 秒数,无单位)
  • timeElapsed: 计算时间段的总时长(单位:秒)

工作原理

  • 通过计算两个时间点之间累积价格的差值,除以时间间隔,得到该时间段的平均价格
  • 时间段越长,价格越稳定,抗操纵能力越强
  • 常用于需要稳定价格参考的场景,如借贷协议的清算价格

优势

  • 抗操纵:需要大量资金和时间才能显著影响价格
  • 无需信任:完全在链上,无需外部数据源
  • 低成本:作为交换的副产品,无需额外 gas

使用场景

  • 借贷协议的价格参考
  • 衍生品协议的结算价格
  • 其他需要价格数据的 DeFi 应用

个人见解与学习

1. 设计的优雅性

Uniswap V2 的设计非常优雅,主要体现在:

  • 简单而强大:核心逻辑只有几百行代码,但功能完整
  • 数学之美:恒定乘积公式简单而有效,自动定价
  • 可组合性:可以轻松与其他协议组合,形成更复杂的应用

2. Gas 优化技巧

代码中有很多 gas 优化技巧:

  • 存储打包:将三个 uint112uint32 打包在一个存储槽中
  • 缓存读取:在函数开始时缓存频繁读取的变量
  • 内联汇编:使用 CREATE2 时使用内联汇编减少 gas

3. 安全设计模式

  • 乐观转移:先转移代币,后验证,减少 gas 消耗
  • 最小流动性锁定:防止首次添加时被全部提取
  • 重入锁:简单而有效的重入保护机制

4. 可扩展性考虑

  • 协议费用:通过可选的协议费用机制,为未来升级留下空间
  • 税费代币支持:考虑到了不标准 ERC20 代币的情况
  • Permit 功能:支持 EIP-2612,提升用户体验

5. 学习收获

通过深入分析 Uniswap V2 的代码,我学到了:

  1. AMM 的工作原理:理解了自动做市商如何通过数学公式自动定价
  2. Solidity 最佳实践:学习了 gas 优化、安全编程等技巧
  3. DeFi 协议设计:理解了如何设计一个安全、高效的 DeFi 协议
  4. 代码可读性:通过添加详细注释,提升了代码的可读性和可维护性

6. 改进建议

虽然 Uniswap V2 已经很优秀,但仍有改进空间:

  • 无常损失:这是 AMM 的固有问题,V3 通过集中流动性部分解决
  • Gas 成本:V3 通过优化进一步降低了 gas 成本
  • 资本效率:V3 通过价格区间提高了资本效率

总结

Uniswap V2 是一个设计精良的 AMM 协议,通过简单的数学公式实现了复杂的去中心化交易功能。其代码简洁、安全、高效,是学习 DeFi 和 Solidity 开发的绝佳案例。

通过深入分析其源代码,我们不仅理解了 AMM 的工作原理,还学到了很多 Solidity 编程和 DeFi 协议设计的最佳实践。这些知识对于理解更复杂的 DeFi 协议(如 Uniswap V3)和开发自己的 DeFi 应用都非常有价值。


参考资料


本文档基于对 Uniswap V2 源代码的深入分析,旨在帮助开发者更好地理解 AMM 协议的工作原理。

  • AI创作
  • 学分: 3
  • 分类: Uniswap
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
乡客
乡客
江湖只有他的大名,没有他的介绍。