Uniswap V2 (UniswapV2Pair)合约

  • 曲弯
  • 发布于 2天前
  • 阅读 56

1.概述UniswapV2配对合约是去中心化交易所UniswapV2的核心组件,实现了自动做市商(AMM)功能。该合约管理两个ERC20代币之间的流动性池,允许用户无需许可地添加/移除流动性并进行代币交换。1.1核心特性恒定乘积做市商:x*y=k模型0.3%交

1. 概述

Uniswap V2 配对合约是去中心化交易所 Uniswap V2 的核心组件,实现了自动做市商(AMM)功能。该合约管理两个 ERC20 代币之间的流动性池,允许用户无需许可地添加/移除流动性并进行代币交换。

1.1 核心特性

  • 恒定乘积做市商:x * y = k 模型
  • 0.3% 交易手续费:从输入金额中扣除
  • 时间加权平均价格(TWAP):内置价格预言机
  • 闪电交换:支持原子交易和闪电贷
  • 协议费用:可选的流动性提供者奖励

1.2 关键参数

参数 说明
MINIMUM_LIQUIDITY 10³ 首次流动性时永久锁定的最小流动性
交易手续费 0.3% 每笔交易收取的费用
协议费用 1/6 流动性增长量的 1/6 作为协议费

2. 合约架构

2.1 继承关系

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20
  • UniswapV2ERC20:实现 LP 代币功能(ERC20 标准)
  • IUniswapV2Pair:定义配对合约的标准接口

2.2 存储布局

// 打包存储(1个存储槽,256位)
uint112 private reserve0;     // 代币0的储备量
uint112 private reserve1;     // 代币1的储备量
uint32  private blockTimestampLast;  // 最后更新时间戳

// 单独存储
uint public price0CumulativeLast;  // 代币0的累积价格
uint public price1CumulativeLast;  // 代币1的累积价格
uint public kLast;  // 最近流动性事件后的 reserve0 * reserve1

2.3 导入的库和接口

// 数学库
using SafeMath  for uint;      // 安全数学运算
using UQ112x112 for uint224;   // 定点数运算

// 接口
IERC20, IUniswapV2Factory, IUniswapV2Callee

3. 核心机制

3.1 恒定乘积公式

reserve0 * reserve1 = constant product (k)
  • 价格由储备比例决定:price0 = reserve1 / reserve0
  • 交易后必须满足:(reserve0 ± Δ0) * (reserve1 ± Δ1) ≥ k

3.2 价格预言机

使用累积价格实现 TWAP:

// 累积价格计算公式
price0CumulativeLast += (reserve1 / reserve0) * timeElapsed
price1CumulativeLast += (reserve0 / reserve1) * timeElapsed

外部合约可通过两个时间点的累积价格差计算平均价格。

3.3 手续费机制

交易手续费

  • 输入金额的 0.3%
  • 计算公式:(balance0 * 1000 - amount0In * 3) * (balance1 * 1000 - amount1In * 3) ≥ reserve0 * reserve1 * 1000²

协议费用

  • 可选的协议费用,由工厂合约的 feeTo地址控制
  • 收取流动性增长量的 1/6
  • 公式:liquidity = totalSupply * (√k - √kLast) / (5 * √k + √kLast)

4. 函数详解

4.1 构造函数和初始化

constructor()

constructor() public {
    factory = msg.sender;
}
  • 设置工厂合约为部署者
  • 确保只有工厂合约能调用 initialize

initialize(address _token0, address _token1)

function initialize(address _token0, address _token1) external
  • 只能由工厂合约调用
  • 设置交易对的两种代币
  • 不包含重复初始化保护

4.2 状态查询

getReserves()

function getReserves() public view returns (uint112, uint112, uint32)

返回当前储备量和最后更新时间戳,用于外部合约获取池子状态。

4.3 流动性管理

mint(address to)- 添加流动性

算法

  1. 计算用户存入的代币数量

  2. 如果是首次添加:

    • liquidity = √(amount0 * amount1) - MINIMUM_LIQUIDITY
    • 永久锁定 MINIMUM_LIQUIDITY到地址 0
  3. 如果是后续添加:

    • liquidity = min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1)
  4. 更新储备和累积价格

首次流动性锁定原因:防止通过极小金额的首次添加操纵价格,然后通过捐赠攻击获取所有流动性。

burn(address to)- 移除流动性

算法

  1. 计算应返还的代币数量:

    • amount0 = liquidity * balance0 / totalSupply
    • amount1 = liquidity * balance1 / totalSupply
  2. 销毁 LP 代币

  3. 返还两种代币给用户

  4. 更新储备

注意:使用实际余额而非储备计算,确保流动性提供者获得应得的手续费。

4.4 代币交换

swap(uint amount0Out, uint amount1Out, address to, bytes data)

核心交换流程

  1. 输入验证

    • 至少有一个输出量 > 0
    • 输出量不超过储备
    • 接收者不能是代币合约本身
  2. 乐观转账

    • 先转出代币给接收者
    • 支持闪电交换(如果 data 长度 > 0)
  3. 恒定乘积检查

    uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
    uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
    require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2));
  4. 闪电交换支持

    • 如果 data.length > 0,调用 IUniswapV2Callee(to).uniswapV2Call()
    • 允许接收者在交换完成前执行任意操作
    • 必须在同一交易内偿还代币

4.5 辅助函数

skim(address to)

提取合约中超过储备的代币余额,用于恢复意外转入的代币。

sync()

强制将储备更新为当前余额,通常在 skim后调用。

4.6 内部函数

_update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1)

更新储备和累积价格的核心函数:

  1. 计算时间差(允许溢出)
  2. 当储备非零时更新累积价格
  3. 设置新的储备和时间戳
  4. 触发 Sync事件

_mintFee(uint112 _reserve0, uint112 _reserve1)

铸造协议费用:

  1. 检查 feeTo地址是否设置
  2. 计算流动性增长
  3. 如果增长为正,铸造 LP 代币给 feeTo地址
  4. 更新 kLast

5. 安全特性

5.1 重入保护

modifier lock() {
    require(unlocked == 1, 'UniswapV2: LOCKED');
    unlocked = 0;
    _;
    unlocked = 1;
}

简单的互斥锁,防止重入攻击。

5.2 溢出保护

  • 使用 OpenZeppelin 的 SafeMath 库
  • 使用 UQ112x112 进行定点数运算,防止价格计算溢出
  • 储备量限制在 uint112 范围内

5.3 输入验证

// 防止无效交换
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');

5.4 转账安全

function _safeTransfer(address token, address to, uint value) private {
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}

同时支持标准和非标准 ERC20 代币。

6. 事件系统

6.1 事件定义

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
    address indexed sender,
    uint amount0In,
    uint amount1In,
    uint amount0Out,
    uint amount1Out,
    address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

6.2 事件触发时机

  • Mint:用户添加流动性时
  • Burn:用户移除流动性时
  • Swap:执行代币交换时
  • Sync:储备更新时(每次 mint、burn、swap 后)

7. Gas 优化技术

7.1 存储优化

  • 存储打包:三个 uint112 和一个 uint32 打包到一个存储槽
  • 内存缓存:频繁访问的存储变量先加载到内存
  • 栈深度管理:使用局部作用域 {}控制栈深度

7.2 计算优化

// Gas 节约:从内存读取而非存储
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
address _token0 = token0;
address _token1 = token1;

7.3 短路评估

if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
    // 仅当条件满足时执行计算
}

8. 数学库

8.1 UQ112x112

用于处理 112.112 定点数,提供高精度的价格计算:

// 编码为 224 位定点数
uint224 q112 = UQ112x112.encode(uint112(value));

// 定点数除法
uint224 result = UQ112x112.uqdiv(numerator, denominator);

8.2 Math

提供整数平方根函数,用于协议费用计算:

uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));

9. 使用示例

9.1 添加流动性

// 1. 用户将代币转入配对合约
token0.transfer(pair, amount0);
token1.transfer(pair, amount1);

// 2. 调用 mint
pair.mint(userAddress);

9.2 执行交换

// 直接调用
pair.swap(amount0Out, amount1Out, to, "");

// 通过路由器调用(推荐)
router.swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline);

9.3 闪电交换

// 1. 执行交换
pair.swap(amount0Out, amount1Out, address(this), data);

// 2. 在 uniswapV2Call 中执行操作
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
    // 使用借入的代币执行操作
    // 必须在本交易内偿还
    uint fee = ((amount0 + amount1) * 3) / 997 + 1;
    token.transfer(pair, amount0 + amount1 + fee);
}

<!--EndFragment-->

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
曲弯
曲弯
0xb51E...CADb
Don't give up if you love it. If you don't, then that's not good either, because one shouldn't do things they don't enjoy.