UniswapV2源码阅读

目录

这系列文章准备详细看看Uniswap的代码,使用的是 Hardhat

零。主要内容

当然!阅读 Uniswap V2 源码是一个非常棒的主题,可以写出一个非常深入和受欢迎的系列。基于第一篇《环境搭建》,后续文章可以按照从核心到外围、从基础到复杂的逻辑展开。

以下是一个可行的系列文章大纲,每一篇都聚焦于一个可验证、可操作的具体模块或功能:

UniswapV2 源码阅读系列文章大纲

第一篇:UniswapV2源码阅读——(一)环境搭建

(已完成)

  • 内容:如何克隆代码、安装依赖、配置 Hardhat 本地开发环境、运行测试节点和基础脚本。
  • 目标:让读者拥有一个可以编译、部署和交互的本地实验环境。

第二篇:UniswapV2源码阅读——(二)工厂合约(UniswapV2Factory)

  • 核心内容
    • 剖析 UniswapV2Factory.sol:构造函数、状态变量(feeTo, feeToSetter, getPair 映射)。
    • 深入 createPair 函数:如何通过 bytescodecreate2 操作码确定性地创建配对合约。
    • 角色权限:feeToSetter 的权限和作用(为协议收费设置接收地址)。
  • 可操作实践
    • 写一个脚本,使用 Factory 合约在本地网络上创建一个新的交易对(如 WETH/USDT)。
    • 验证 getPair 映射是否正确更新。
    • 查询创建的 Pair 合约地址。

第三篇:UniswapV2源码阅读——(三)配对合约核心(UniswapV2Pair)

  • 核心内容
    • 合约结构:状态变量(reserve0, reserve1, blockTimestampLast, price0CumulativeLast)。
    • 核心功能:mint (添加流动性), burn (移除流动性), swap (交易)。
    • 深入理解 _update 函数:如何更新准备金并累积价格。
  • 可操作实践
    • 在上一篇创建的 Pair 合约中,模拟执行 mintswap 操作。
    • 编写脚本查询交易前后的储备金变化,直观理解 AMM 做市。

第四篇:UniswapV2源码阅读——(四)价格预言机

  • 核心内容
    • 原理阐述:如何通过“累积价格”提供防操纵的链上价格数据。
    • 代码分析:price0CumulativeLastprice1CumulativeLast 的计算方式。
    • 精度与时间窗口:讲解 UQ112x112 库如何用 224 位存储浮点数,以及时间差对价格计算的影响。
  • 可操作实践
    • 在几次 swap 操作后,编写脚本计算指定时间窗口内的平均价格。
    • 与 Chainlink 等预言机进行简单对比,理解其优缺点。

第五篇:UniswapV2源码阅读——(五)路由合约(UniswapV2Router)

  • 核心内容
    • 角色定位:为什么需要 Router?它是用户的入口,负责处理复杂的交易路径和 ETH 包装。
    • 核心函数:addLiquidity, removeLiquidity, swapExactTokensForTokens
    • 安全性:deadline 参数的作用、防止前端运行(Front-running)的机制。
  • 可操作实践
    • 编写脚本,通过 Router 合约完成一次完整的“添加流动性 -> 交易 -> 移除流动性”流程。
    • 对比直接调用 Pair 合约和通过 Router 合约的差异。

第六篇:UniswapV2源码阅读——(六)合约部署与初始化流程

  • 核心内容
    • 完整复盘:从零开始部署全套 Uniswap V2 合约的步骤。
    • 顺序依赖:先部署 WETH -> 再部署 Factory -> 设置 FeeTo -> 部署 Router
    • 权限管理:feeToSetter 地址的转移和多签设置最佳实践。
  • 可操作实践
    • 编写一个完整的部署脚本,一次性在本地或测试网部署所有合约。
    • 验证整套系统能否正常工作。

第七篇:UniswapV2源码阅读——(七)闪电贷(Flash Swap)原理解析

  • 核心内容
    • 概念讲解:什么是闪电贷?与 Aave 等平台的闪电贷区别是什么?
    • 代码追踪:剖析 swap 函数中的 data 参数和 uniswapV2Call 回调机制。
    • 应用场景:套利、自我清算、无抵押贷款。
  • 可操作实践
    • 编写一个可执行闪电贷的合约。
    • 设计一个套利场景,并编写脚本演示一次成功的闪电贷套利操作。

第八篇:UniswapV2源码阅读——(终)总结与展望

  • 核心内容
    • 架构总结:回顾核心合约之间的交互关系和数据流向。
    • 安全设计:重入锁、精度处理、权限控制等安全考虑。
    • 经济模型:0.3% 手续费的去向(LP 提供者 vs. 协议)。
    • 对比 V3:简要介绍 V3 的集中流动性等创新,作为系列的延伸。

这个结构从易到难,每一篇都建立在上一篇的基础上,并且每一篇都有具体的代码和实操环节,避免了纯理论分析,完全符合你“一定要可行”的要求。祝你写作顺利!这是一个非常有价值的系列。

一、环境搭建

这是第一篇,主要讲环境搭建和部署的部分。

首先使用 npm 安装 hardhat

接下来初始化 Hardhat 项目

  1. 创建项目文件夹并进入
mkdir UniswapV2_SourceRead
cd UniswapV2_SourceRead
  1. 初始化 Hardhat 项目
% npx hardhat --init

 █████  █████                         ███  ███                  ███      ██████
░░███  ░░███                         ░███ ░███                 ░███     ███░░███
 ░███   ░███   ██████  ████████   ███████ ░███████    ██████  ███████  ░░░  ░███
 ░██████████  ░░░░░███░░███░░███ ███░░███ ░███░░███  ░░░░░███░░░███░      ████░
 ░███░░░░███   ███████ ░███ ░░░ ░███ ░███ ░███ ░███   ███████  ░███      ░░░░███
 ░███   ░███  ███░░███ ░███     ░███ ░███ ░███ ░███  ███░░███  ░███ ███ ███ ░███
 █████  █████░░███████ █████    ░░███████ ████ █████░░███████  ░░█████ ░░██████
░░░░░  ░░░░░  ░░░░░░░ ░░░░░      ░░░░░░░ ░░░░ ░░░░░  ░░░░░░░    ░░░░░   ░░░░░░
 
👷 Welcome to Hardhat v3.0.6 👷

✔ Which version of Hardhat would you like to use? · hardhat-2
✔ Where would you like to initialize the project?

Please provide either a relative or an absolute path: · .
✔ What type of project would you like to initialize? · mocha-ethers-js
✨ Template files copied ✨
✔ You need to install the necessary dependencies using the following command:
npm install --save-dev "@nomicfoundation/hardhat-chai-matchers@^2.0.0" "@nomicfoundation/hardhat-ethers@^3.0.0" "@nomicfoundation/hardhat-ignition@^0.15.0" "@nomicfoundation/hardhat-ignition-ethers@^0.15.0" "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-toolbox@^6.0.0" "@nomicfoundation/hardhat-verify@^2.0.0" "@typechain/ethers-v6@^0.5.0" "@typechain/hardhat@^9.0.0" "chai@^4.2.0" "ethers@^6.4.0" "hardhat@^2.14.0" "hardhat-gas-reporter@^2.3.0" "solidity-coverage@^0.8.0" "typechain@^8.3.0"

Do you want to run it now? (Y/n) · true
....

下载Uniswap V2的源码并安装相应的依赖

npm install @uniswap/lib
npm install @uniswap/v2-core

git clone https://github.com/Uniswap/uniswap-v2-core.git
git clone https://github.com/Uniswap/uniswap-v2-periphery.git
mv uniswap-v2-core contracts/
mv uniswap-v2-periphery contracts/

修改hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    compilers: [
      {
        // 用于 uniswap-v2-core
        version: "0.5.16", 
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
      {
        // 用于 uniswap-v2-periphery
        version: "0.6.6", 
        settings: {
          optimizer: {
            enabled: true,
            runs: 200,
          },
        },
      },
    ],
  },
  networks: {
    hardhat: {
      // 默认设置,可以不写
    },
  },
};

然后编译:

% npx hardhat compile                       
Compiled 43 Solidity files successfully (evm target: istanbul).

接下来就可以启动一个本地的以太坊测试网络,默认创建 ​20 个测试账户,并为每个账户预分配 ​10,000 个假的 ETH:

% npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

...

写个测试的代码:

// scripts/sc.js
const { ethers } = require("hardhat");

async function main() {
  console.log("🔍 获取本地网络账户信息...\n");

  // 获取所有账户(包括部署者)
  const accounts = await ethers.getSigners();
  const provider = ethers.provider;

  console.log(`📝 共找到 ${accounts.length} 个账户\n`);

  // 获取部署者账户(第一个账户)
  const deployer = accounts[0];
  const deployerBalance = await provider.getBalance(deployer.address);
  
  console.log("👤 部署者账户:");
  console.log(`   地址: ${deployer.address}`);
  console.log(`   余额: ${ethers.formatEther(deployerBalance)} ETH\n`);

  // 获取所有测试账户及其余额
  console.log("👥 所有测试账户:");
  for (let i = 0; i < accounts.length; i++) {
    const account = accounts[i];
    const balance = await provider.getBalance(account.address);
    
    console.log(`   ${i + 1}. 地址: ${account.address}`);
    console.log(`      余额: ${ethers.formatEther(balance)} ETH`);
  }

  console.log("\n✅ 账户信息获取完成");
}

// 执行并处理错误
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

运行脚本:

% npx hardhat run scripts/sc.js --network localhost

🔍 获取本地网络账户信息...

📝 共找到 20 个账户

👤 部署者账户:
   地址: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
   余额: 10000.0 ETH

👥 所有测试账户:
   1. 地址: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
      余额: 10000.0 ETH
   2. 地址: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
      余额: 10000.0 ETH
   3. 地址: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
      余额: 10000.0 ETH
   4. 地址: 0x90F79bf6EB2c4f870365E785982E1f101E93b906
      余额: 10000.0 ETH
   5. 地址: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
      余额: 10000.0 ETH
   6. 地址: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc
      余额: 10000.0 ETH
   7. 地址: 0x976EA74026E726554dB657fA54763abd0C3a0aa9
      余额: 10000.0 ETH
   8. 地址: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955
      余额: 10000.0 ETH
   9. 地址: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
      余额: 10000.0 ETH
   10. 地址: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
      余额: 10000.0 ETH
   11. 地址: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
      余额: 10000.0 ETH
   12. 地址: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788
      余额: 10000.0 ETH
   13. 地址: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a
      余额: 10000.0 ETH
   14. 地址: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec
      余额: 10000.0 ETH
   15. 地址: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097
      余额: 10000.0 ETH
   16. 地址: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71
      余额: 10000.0 ETH
   17. 地址: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30
      余额: 10000.0 ETH
   18. 地址: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E
      余额: 10000.0 ETH
   19. 地址: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0
      余额: 10000.0 ETH
   20. 地址: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199
      余额: 10000.0 ETH

✅ 账户信息获取完成

UniswapV2源码阅读——(二)合约架构

一、整体架构分层

Uniswap V2 的合约架构采用分层设计,将核心层(Core)与应用层(Periphery)分离,既保证了资金安全又提供了良好的用户体验。本文将系统介绍 Uniswap V2 的整体合约架构,为后续深入分析各合约细节奠定基础。

1. 核心层(Core Contracts)

  • 定位:直接管理用户资产的基础合约,代码高度精简,仅包含协议运行所必需的最小功能集。
  • 特点:安全至上、功能最小化。所有用户资金和流动性均由核心合约直接持有,因此其逻辑必须尽可能简单,以最大限度减少攻击面和潜在漏洞。
  • 职责:实现自动做市商(AMM)的核心机制,包括交易对创建、流动性管理、代币兑换、LP 代币发行等。
  • 核心合约
    • UniswapV2Factory:交易对工厂合约,负责创建和注册交易对。
    • UniswapV2Pair 继承自 UniswapV2ERC20,因此每个 Pair 合约自身就是一个 ERC20 代币,其代币代表该池的流动性份额(即 LP 代币)。
    • UniswapV2ERC20 并非独立部署的合约,而是作为基类被 UniswapV2Pair 继承,用于提供标准 ERC20 接口及 EIP-2612 支持。

2. 应用层(Periphery Contracts)

  • 定位:提供用户友好的交互接口,封装核心层的底层操作。
  • 特点:功能丰富、便于集成。- 外围合约不持久持有用户资产,仅在交易执行期间临时中转。所有用户资金最终由核心层(Pair)持有,外围合约无权长期控制或挪用资产。
  • 职责:简化用户操作,例如自动处理 ETH 与 WETH 的转换、支持多跳兑换路径、集成滑点保护与交易时限等安全机制。
  • 主要组件
    • UniswapV2Router02:主路由合约,是用户最常用的交互入口。
    • 工具库(Libraries):如 TransferHelper 用于安全转账,UniswapV2Library 提供路径计算等辅助函数。
    • 示例合约:闪电贷、预言机等用例演示

这种分离设计使得核心合约可以保持不可变且高度安全,而外围合约可根据需求迭代升级,而不影响底层资产安全。

二、核心层合约详解

1. 工厂合约(UniswapV2Factory)

角色:作为所有交易对的中央注册中心和唯一创建入口。协议部署时仅需部署此合约,并指定一个 feeToSetter 地址。

核心功能

  • 通过 createPair 为任意两个 ERC20 代币(或 WETH)创建唯一的交易对合约。
  • 管理协议手续费接收地址 feeTo(默认为零地址,即无协议收入)。
contract UniswapV2Factory is IUniswapV2Factory {
    // 状态变量
    address public feeTo;         // 协议手续费接收地址(可选)
    address public feeToSetter;   // 拥有权限设置或更改 `feeTo`地址的账户
    address[] public allPairs;    // 存储所有已创建交易对(Pair)的合约地址
    
    // 事件,当新交易对被创建时触发
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);
    
    // 创建新交易对
    function createPair(address tokenA, address tokenB) 
        external returns (address pair);
    
    // 获取已存在的交易对
    function getPair(address tokenA, address tokenB) 
        external view returns (address pair);
}

2. 交易对合约(UniswapV2Pair)

角色:实际的流动性池,执行 AMM 逻辑,并发行 LP 代币。

核心功能

  • swap: 执行代币兑换,并收取千分之三(0.3%)的交易手续费,该费用全部注入流动性池,提升 LP 代币价值。
  • mint / burn: 管理流动性增减,铸造/销毁 LP 代币。
  • 价格预言机: 提供时间加权平均价格(TWAP)功能。TWAP 通过在每次流动性操作 (mint, burn) 或兑换 (swap) 触发储备更新 (_update) 时,计算自上次更新以来的时间间隔 (timeElapsed),并按照当前储备比例 (reserve1 / reserve0reserve0 / reserve1) 乘以时间间隔来更新累积价格 (price0CumulativeLastprice1CumulativeLast)。这确保了累积价格反映了价格随时间的变化,但更新依赖于链上操作的发生。
  • 闪电贷: 通过 swapdata 参数回调实现,用户可在单笔交易中借出并归还资产。

双重身份:它本身也是一个符合 ERC20 标准的代币合约,其发行的代币(LP代币)代表了流动性提供者在池中的份额。

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
    // 核心状态
    uint112 private reserve0;     // 代币0储备量
    uint112 private reserve1;     // 代币1储备量
    uint32  private blockTimestampLast; // 上次更新时间戳(用于 TWAP)
    
    // 添加流动性
    function mint(address to) external returns (uint liquidity);
    // 移除流动性
    function burn(address to) external returns (uint amount0, uint amount1);
    // 执行交易
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}

3. 流动性代币合约(UniswapV2ERC20)

角色:为 UniswapV2Pair 合约提供 ERC20 代币的标准实现,使其发行的 LP 代币可以像普通代币一样被转移和交易。

核心功能

  • 标准ERC20功能:transfer, approve, balanceOf 等。
  • EIP-2612 支持:通过 permit 函数实现离线签名授权,减少用户 Gas 消耗。
contract UniswapV2ERC20 is IUniswapV2ERC20 {
    // ERC20标准功能
    function transfer(address to, uint value) external returns (bool);
    function approve(address spender, uint value) external returns (bool);
    // 离线签名授权
    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
}

三、应用层核心组件

外围合约用于简化用户与核心合约的交互,其中最重要的是路由合约和库合约。

路由合约(UniswapV2Router02)

角色:用户与协议交互最常用的主入口,封装核心层操作。虽然 Pair 合约已能完成所有交易操作,但路由合约将这些操作整合,极大地方便了用户和前端应用。

核心功能

  1. 流动性管理: 提供 addLiquidityremoveLiquidity 等方法,支持 ETH/WETH 自动转换。
  2. 多跳兑换:如 swapExactTokensForTokens,支持路径 [A, B, C] 实现 A→B→C 兑换。
  3. 安全特性: 强制用户指定 amountOutMin(防滑点)和 deadline(防交易延迟)。

版本说明UniswapV2Router02 相比 01 版本,改进了对部分特殊代币(如 fee-on-transfer 代币)的兼容性,但并非完全支持所有非标准 ERC20。

contract UniswapV2Router02 is IUniswapV2Router02 {
    // ETH包装器地址
    address public immutable WETH;  
    
    // 添加移除流动性
    function addLiquidity() external payable;
    function removeLiquidity() external;
    
    // 多路径兑换
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external;
}

库合约(Library Contracts)

角色:提供可重用的工具函数,例如计算交易对地址、计算兑换数量等。如:

  • UniswapV2Library: 提供 Pair 地址计算、兑换量估算、路径验证等静态函数。
  • TransferHelper: 封装 safeTransfer / safeTransferFrom,避免部分 ERC20 实现不返回 bool 的问题。
  • SafeMath: 数学运算安全保护

四、合约交互流程

一个典型的 Uniswap V2 交互流程如下:

  1. 创建交易对(按需)

    • 用户不直接调用 Factory.createPair,而是通过 Router(如 addLiquidity)间接触发。
    • Router 内部检查 Pair 是否存在:若不存在,则调用 Factory.createPair(tokenA, tokenB)
    • Factory 仅在 Pair 不存在时,使用 CREATE2 部署新 Pair 合约,其地址由排序后的代币地址和 init code 哈希唯一确定。
  2. 添加流动性

    • 用户调用 Router.addLiquidity(或 addLiquidityETH),传入两种代币(或 ETH)及数量,并授权代币给 Router。
    • Router 将用户资产(代币或 WETH)转入 Pair 合约。
    • Router 调用 Pair.mint(userAddress),Pair 直接向用户地址铸造 LP 代币,不经过 Router 中转。
  3. 执行兑换

    • 用户调用 Router.swapExactTokensForTokens 等函数,指定兑换路径、输入量、最小输出量(amountOutMin)和截止时间(deadline)。
    • Router 按路径依次调用各 Pair 的 swap(amount0Out, amount1Out, userAddress, "")
    • 每个 Pair 直接将输出代币转账至用户指定地址(通常为用户自身),完成多跳兑换。
  4. 移除流动性

    • 用户调用 Router.removeLiquidity(或 removeLiquidityETH),并将 LP 代币授权或转入 Router。
    • Router 调用 Pair.burn(userAddress)
    • Pair 销毁 LP 代币,并直接将对应比例的两种代币(含累积手续费)转账给用户地址
graph TD
    subgraph 用户 User
        UserA[用户]
    end

    subgraph 应用层 Periphery
        Router[UniswapV2Router02]
    end

    subgraph 核心层 Core
        Factory[UniswapV2Factory]
        Pair[UniswapV2Pair]
    end

    UserA -- 1. 授权代币
或发送ETH --> Router UserA -- 2. 调用Router函数
(add/remove/swap) --> Router Router -- 3a. 若Pair不存在:
创建Pair --> Factory Factory -- 3b. 返回Pair地址 --> Router Router -- 4. 转入输入资产到Pair --> Pair Router -- 5. 调用Pair函数 --> Pair Pair -- 6. 执行核心逻辑 --> Pair Pair -- 7. 将输出资产
直接发送给用户 --> UserA classDef user fill:#d4f7e5,stroke:#2e8b57; classDef periphery fill:#e6f0ff,stroke:#1e5dbf; classDef core fill:#fff0f0,stroke:#b33c3c; class UserA user class Router periphery class Factory,Pair core
graph TD
    subgraph 用户 User
        UserA[用户]
    end

    subgraph 应用层 Periphery
        Router[UniswapV2Router02]
    end

    subgraph 核心层 Core
        Factory[UniswapV2Factory]
        Pair[UniswapV2Pair]
    end

    UserA -- 1. 授权代币
或发送ETH --> Router UserA -- 2. 调用Router函数
(add/remove/swap) --> Router Router -- 3a. 若Pair不存在:
创建Pair --> Factory Factory -- 3b. 返回Pair地址 --> Router Router -- 4. 转入输入资产到Pair --> Pair Router -- 5. 调用Pair函数 --> Pair Pair -- 6. 执行核心逻辑 --> Pair Pair -- 7. 将输出资产
直接发送给用户 --> UserA classDef user fill:#d4f7e5,stroke:#2e8b57; classDef periphery fill:#e6f0ff,stroke:#1e5dbf; classDef core fill:#fff0f0,stroke:#b33c3c; class UserA user class Router periphery class Factory,Pair core
graph TD
    subgraph 用户 User
        UserA[用户]
    end

    subgraph 应用层 Periphery
        Router[UniswapV2Router02]
    end

    subgraph 核心层 Core
        Factory[UniswapV2Factory]
        Pair[UniswapV2Pair]
    end

    UserA -- 1. 授权代币
或发送ETH --> Router UserA -- 2. 调用Router函数
(add/remove/swap) --> Router Router -- 3a. 若Pair不存在:
创建Pair --> Factory Factory -- 3b. 返回Pair地址 --> Router Router -- 4. 转入输入资产到Pair --> Pair Router -- 5. 调用Pair函数 --> Pair Pair -- 6. 执行核心逻辑 --> Pair Pair -- 7. 将输出资产
直接发送给用户 --> UserA classDef user fill:#d4f7e5,stroke:#2e8b57; classDef periphery fill:#e6f0ff,stroke:#1e5dbf; classDef core fill:#fff0f0,stroke:#b33c3c; class UserA user class Router periphery class Factory,Pair core

五、关键设计思想

  1. 安全边界控制

    • 核心合约功能最小化(逻辑简洁,无外部依赖)
    • 用户资金仅由核心合约持有,外围合约无资产所有权
  2. 确定性地址生成

    • 通通过 CREATE2 部署 Pair 合约,确保地址可预先计算。
    • 地址由工厂地址、init code 哈希及排序后代币地址的哈希(作为 salt)共同确定,无需链上查询即可推导 Pair 地址。
  3. 协议可扩展性

    • 协议费开关:通过 feeTo 激活协议收入(0.05%)
    • 外围合约可独立升级(如部署新 Router)
  4. Gas 优化

    • 储备量使用 uint112(配合 uint32 时间戳,共占 3 个存储槽)
    • 减少 SSTORE:状态更新集中处理,避免中间写入

UniswapV2源码阅读——(三)工厂合约(UniswapV2Factory)

1. 工厂合约简介

UniswapV2Factory 是 Uniswap V2 的核心组件之一,负责创建交易对合约(UniswapV2Pair)。

角色:作为所有交易对的中央注册中心和唯一创建入口。协议部署时仅需部署此合约,并指定一个 feeToSetter 地址。

核心功能

  • 通过 createPair 为任意两个 ERC20 代币(或 WETH)创建唯一的交易对合约。
  • 管理协议手续费接收地址 feeTo(默认为零地址,即无协议收入)。

2. 源码阅读

2.1 关键变量

address public feeTo; // 收取手续费的地址,当 feeTo == address(0)时:​协议费用关闭​
address public feeToSetter; // 权限管理地址
mapping(address => mapping(address => address)) public getPair; // token0 -> token1 -> pair address
address[] public allPairs; // 所有交易对地址列表

event PairCreated(address indexed token0, address indexed token1, address pair, uint);
  • getPair[token0][token1]:快速查找 token0token1 对应的交易对地址
  • allPairs:维护所有创建的交易对地址
  • PairCreated:交易对创建时触发的事件

2.2 函数列表与解析

2.2.1. constructor(address _feeToSetter) public

constructor(address _feeToSetter) public {
    feeToSetter = _feeToSetter;
}
  • 作用:合约构造函数,初始化 feeToSetter 地址
  • 参数_feeToSetter 是唯一能设置 feeTo 的地址

2.2.2. setFeeTo(address _feeTo) external

function setFeeTo(address _feeTo) external {
    require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
    feeTo = _feeTo;
}
  • 作用:设置手续费接收地址
  • 参数_feeTo 是新的手续费地址
  • 权限控制:只有 feeToSetter 可调用

2.2.3. setFeeToSetter(address _feeToSetter) external

function setFeeToSetter(address _feeToSetter) external {
    require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
    feeToSetter = _feeToSetter;
}
  • 作用:设置 feeToSetter 地址(即未来能设置 feeTo 的地址)
  • 参数_feeToSetter 是新的 feeToSetter
  • 权限控制:只有当前 feeToSetter 可调用
  • 说明setFeeToSetter 实现了权限的移交机制,允许当前的 feeToSetter 将权限转移给新的账户,从而防止权限被永久锁定。

2.2.4. allPairsLength() external view returns (uint)

function allPairsLength() external view returns (uint) {
    return allPairs.length;
}
  • 作用:返回当前创建的交易对数量
  • 返回值uint 类型的交易对总数

2.2.5. createPair(address tokenA, address tokenB) external returns (address pair)

function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}
  • 作用:创建新的交易对合约
  • 参数
    • tokenA, tokenB:要创建交易对的两种代币地址
  • 返回值:新创建的交易对合约地址
  • 整体流程
    1. 校验 tokenA != tokenB
    2. 排序 token0 < token1,避免重复创建
    3. 使用 create2 预计算交易对地址
    4. 初始化交易对合约
    5. 更新 getPair 映射和 allPairs 列表
    6. 触发 PairCreated 事件

其中 CREATE2 是以太坊智能合约中用于创建新合约的字节码指令。它需要四个参数:

  • ​value​ (uint256):发送给新合约的 ETH 数量
  • ​offset​ (uint256):初始化代码在内存中的起始位置
  • ​size​ (uint256):初始化代码的长度
  • ​salt​ (uint256):用于地址计算的 32 字节盐值

3. 本地测试验证

3.1 部署工厂合约

这个脚本的功能是准备一个账户,使用这个账户部署 Factory 合约,然后看下账户的余额

const { ethers } = require("hardhat");

async function main() {
    // 准备账户
    const [deployer, user] = await ethers.getSigners();
    console.log("[+] 部署者账户:", deployer.address);
    console.log("    用户账户:", user.address);
    console.log("    部署者余额:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)), "ETH");


    // 部署 Factory 合约
    const Factory = await ethers.getContractFactory("contracts/uniswap-v2-core/contracts/UniswapV2Factory.sol:UniswapV2Factory");
    const factory = await Factory.deploy(deployer.address);
    await factory.waitForDeployment();
    const FACTORY_ADDRESS = await factory.getAddress();
    console.log("[+] Factory 合约地址:", FACTORY_ADDRESS);

    // 账户余额
    console.log("[+] 部署者账户:", deployer.address);
    console.log("    用户账户:", user.address);
    console.log("    部署者余额:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)), "ETH");

}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
}).finally(() => {
    console.log("脚本执行完成!");
});
// npx hardhat run scripts/deploy-factory.js --network localhost

可以看到两个终端的输出:

  Contract deployment: UniswapV2Factory
  Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3
  Transaction:         0xf75cadcf3e2efb3dc1426023c16786738b80c3e775f1d5a883d196ad91756e26
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  Value:               0 ETH
  Gas used:            2414728 of 30000000
  Block #1:            0x2bcc0018790c91d6bcb412e211739ed6abfaddbb87e972c967d11fe94a5668f1



% npx hardhat run scripts/tmp.js --network localhost
[+] 部署者账户: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
    用户账户: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
    部署者余额: 10000.0 ETH
[+] Factory 合约地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3
[+] 部署者账户: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
    用户账户: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
    部署者余额: 9999.997325877390625 ETH
脚本执行完成!

3.2 创建合约对并监听事件

在下面的脚本中,创建合约并监听PairCreated事件:

const { ethers } = require("hardhat");

async function main() {
    // 准备账户
    const [deployer, user] = await ethers.getSigners();
    console.log("[+] 部署者账户:", deployer.address);
    console.log("    用户账户:", user.address);
    console.log("    部署者余额:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)), "ETH");


    // 部署 Factory 合约
    const Factory = await ethers.getContractFactory("contracts/uniswap-v2-core/contracts/UniswapV2Factory.sol:UniswapV2Factory");
    const factory = await Factory.deploy(deployer.address);
    await factory.waitForDeployment();
    const FACTORY_ADDRESS = await factory.getAddress();
    console.log("[+] Factory 合约地址:", FACTORY_ADDRESS);

    // 部署测试代币
    const ERC20Contract = await ethers.getContractFactory(
        "contracts/uniswap-v2-periphery/contracts/test/ERC20.sol:ERC20"
    );

    const tokenA = await ERC20Contract.deploy(ethers.parseEther("100"));
    await tokenA.waitForDeployment();
    const tokenAAddress = await tokenA.getAddress();
    console.log("[+] TokenA 部署成功! 地址:", tokenAAddress);

    const tokenB = await ERC20Contract.deploy(ethers.parseEther("10000"));
    await tokenB.waitForDeployment();
    const tokenBAddress = await tokenB.getAddress();
    console.log("[+] TokenB 部署成功! 地址:", tokenBAddress);



    // 监听 PairCreated 事件,使用一次性监听
    factory.once("PairCreated", (tokenA, tokenB, pair, pairIndex) => {
        console.log("交易对创建成功!");
        console.log("  TokenA:", tokenA);
        console.log("  TokenB:", tokenB);
        console.log("  Pair Address:", pair);
        console.log("  Index:", pairIndex);
    });

    // 创建交易对
    const tx = await factory.createPair(tokenAAddress, tokenBAddress);
    await tx.wait();
    const pairAddress = await factory.getPair(tokenAAddress, tokenBAddress);
    console.log("[+] 创建交易对,地址:", pairAddress);
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
}).finally(() => {
    console.log("脚本执行完成!");
});
// npx hardhat run scripts/deploy-factory.js --network localhost
% npx hardhat run scripts/tmp.js --network localhost
[+] 部署者账户: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
    用户账户: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
    部署者余额: 10000.0 ETH
[+] Factory 合约地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3
[+] TokenA 部署成功! 地址: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
[+] TokenB 部署成功! 地址: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
[+] 创建交易对,地址: 0xb362C9fd281047185623Ee91c5Ff405274bbEEFe
脚本执行完成!
交易对创建成功!
  TokenA: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
  TokenB: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  Pair Address: 0xb362C9fd281047185623Ee91c5Ff405274bbEEFe
  Index: 1n

3.3 修改 feeToSetter

这个脚本用于修改 Factory 合约的 feeToSetter

const { ethers } = require("hardhat");

async function main() {
    // 准备账户
    const [deployer, user] = await ethers.getSigners();
    console.log("[+] 部署者账户:", deployer.address);
    console.log("    用户账户:", user.address);
    console.log("    部署者余额:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)), "ETH");

    // 部署 Factory 合约
    const Factory = await ethers.getContractFactory("contracts/uniswap-v2-core/contracts/UniswapV2Factory.sol:UniswapV2Factory");
    const factory = await Factory.deploy(deployer.address);
    await factory.waitForDeployment();
    const FACTORY_ADDRESS = await factory.getAddress();
    console.log("[+] Factory 合约地址:", FACTORY_ADDRESS);

    // 1. 验证初始 feeToSetter
    const initialFeeToSetter = await factory.feeToSetter();
    console.log("✅ 初始 feeToSetter:", initialFeeToSetter);
    console.log("   - 是否部署者?", initialFeeToSetter === deployer.address ? "是" : "否");

    // 2. 修改 feeToSetter
    console.log("\n[+] 修改 feeToSetter 到用户地址...");
    const tx = await factory.setFeeToSetter(user.address);
    await tx.wait();
    console.log("   - 交易哈希:", tx.hash);

    // 3. 验证修改结果
    const newFeeToSetter = await factory.feeToSetter();
    console.log("\n✅ 新 feeToSetter:", newFeeToSetter);
    console.log("   - 是否用户?", newFeeToSetter === user.address ? "是" : "否");

    // 4. 尝试从部署者再次修改(应失败)
    console.log("\n[+] 测试权限验证(部署者应不再有权修改)...");
    try {
        const invalidTx = await factory.setFeeToSetter(deployer.address);
        await invalidTx.wait();
        console.log("❌ 测试失败:部署者仍能修改 feeToSetter");
    } catch (error) {
        console.log("✅ 测试通过:部署者无法修改 feeToSetter");
        console.log("   - 错误信息:", error.reason || error.message);
    }

    // 5. 尝试从用户修改
    console.log("\n[+] 测试新 feeToSetter 权限(用户应能修改)...");
    try {
        // 使用用户账户连接工厂合约
        const factoryAsUser = factory.connect(user);
        const userTx = await factoryAsUser.setFeeToSetter(deployer.address);
        await userTx.wait();
        console.log("✅ 测试通过:用户成功修改 feeToSetter");

        // 验证修改结果
        const finalFeeToSetter = await factory.feeToSetter();
        console.log("   - 最终 feeToSetter:", finalFeeToSetter);
        console.log("   - 是否部署者?", finalFeeToSetter === deployer.address ? "是" : "否");
    } catch (error) {
        console.log("❌ 测试失败:用户无法修改 feeToSetter");
        console.log("   - 错误信息:", error.reason || error.message);
    }

}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
}).finally(() => {
    console.log("\n脚本执行完成!");
    process.exit(0); // 确保脚本退出
});

运行后可以看到输出如下:

% npx hardhat run scripts/tmp.js --network localhost
[+] 部署者账户: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
    用户账户: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
    部署者余额: 10000.0 ETH
[+] Factory 合约地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3
✅ 初始 feeToSetter: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
   - 是否部署者? 是

[+] 修改 feeToSetter 到用户地址...
   - 交易哈希: 0xa25858adc4e98b91cc314456920f202c6f5e2b284c10f83ce131f1a4e64ca515

✅ 新 feeToSetter: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
   - 是否用户? 是

[+] 测试权限验证(部署者应不再有权修改)...
✅ 测试通过:部署者无法修改 feeToSetter
   - 错误信息: Error: VM Exception while processing transaction: reverted with reason string 'UniswapV2: FORBIDDEN'

[+] 测试新 feeToSetter 权限(用户应能修改)...
✅ 测试通过:用户成功修改 feeToSetter
   - 最终 feeToSetter: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
   - 是否部署者? 是

脚本执行完成!

4. 小结

  • createPair 使用 create2 预计算交易对地址,地址可预测
  • getPair 映射避免重复创建交易对
  • PairCreated 事件可用于前端监听交易对创建
  • allPairs 列表可用于遍历所有交易对

UniswapV2源码阅读——(四)配对合约(UniswapV2Pair)

1. 交易对合约(UniswapV2Pair)简述

这部分之前介绍过

角色:实际的流动性池,执行 AMM 逻辑,并发行 LP 代币。

核心功能

  • swap: 执行代币兑换,并收取千分之三(0.3%)的交易手续费,该费用全部注入流动性池,提升 LP 代币价值。
  • mint / burn: 管理流动性增减,铸造/销毁 LP 代币。
  • 价格预言机: 提供时间加权平均价格(TWAP)功能。TWAP 通过在每次流动性操作 (mint, burn) 或兑换 (swap) 触发储备更新 (_update) 时,计算自上次更新以来的时间间隔 (timeElapsed),并按照当前储备比例 (reserve1 / reserve0reserve0 / reserve1) 乘以时间间隔来更新累积价格 (price0CumulativeLastprice1CumulativeLast)。这确保了累积价格反映了价格随时间的变化,但更新依赖于链上操作的发生。
  • 闪电贷: 通过 swapdata 参数回调实现,用户可在单笔交易中借出并归还资产。

双重身份:它本身也是一个符合 ERC20 标准的代币合约,其发行的代币(LP代币)代表了流动性提供者在池中的份额。

2. 源码阅读

2.1 核心状态变量

以下是 UniswapV2Pair 合约中核心状态变量

uint public constant MINIMUM_LIQUIDITY = 10 ** 3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

address public factory;
address public token0;
address public token1;
uint112 private reserve0; 
uint112 private reserve1; 
uint32 private blockTimestampLast; 
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; 
  • MINIMUM_LIQUIDITY: 作用是防止初始流动性被完全移除。在初次mint的时候,会铸造 sqrt(amount0 × amount1) 的 LP 代币,但其中 1000(即 10^3)个会被销毁(mintaddress(0))。
  • SELECTOR: 用于在 swap 函数中安全调用代币的 transfer 方法。swap 支持“先转出代币,再由接收方回调补足”的闪电贷模式。为避免调用恶意合约的 transfer,Uniswap V2 通过 SELECTOR 确保只调用标准 ERC20 的 transfer 函数
  • factory: 记录创建该 Pair 的工厂合约地址(即 UniswapV2Factory)
  • token0,token1: 组成该交易对的两个 ERC20 代币地址
  • reserve0,reserve1: 分别存储 token0token1 的当前储备量
  • blockTimestampLast: 记录上次更新储备的时间戳
  • price0CumulativeLast,price1CumulativeLast: 分别累计 token1/token0token0/token1 的价格积分值(即价格对时间的积分)。供外部调用者(如预言机)通过 (currentCumulative - lastCumulative) / timeElapsed 计算 TWAP
  • kLast: 记录上次收取协议手续费时的 reserve0 × reserve1

2.2 getReserves()

获取两个代币的储备量和上次更新储备的时间

function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
    _reserve0 = reserve0;
    _reserve1 = reserve1;
    _blockTimestampLast = blockTimestampLast;
}

2.3 getReserves()

用于安全转账 ERC20 代币,并确保转账失败时能正确回滚交易。

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');
}

2.4 _update()

_update方法用于更新储备金,整体流程如下:

首先进行溢出检查,uint112(-1)表示 uint112 的最大值(2^112 - 1),确保代币余额不超过 uint112 的存储上限。然后获取时间戳计算出当前时间距离上次更新后经过的时间。

计算token1 相对于 token0 的累积价格token0 相对于 token1 的累积价格, 即price0CumulativeLastprice0CumulativeLast。其中,UQ112x112.encode(x) 表示将 uint112 转换为 Q112x112 定点数。.uqdiv(x)表示无溢出除法

对时间进行加权(即* timeElapsed),相当于计算了从上次更新到当前时刻这段时间内,价格对时间的“积分”。它代表了在这段时间内,价格曲线下的“面积”,然后进行累加。也就是在计算TWAP。

price0CumulativeLast因此成为了一个 ​从某个起始时间点开始,token0 相对于 token1 的价格对时间的累计积分。它是一个随时间单调递增(或递减,如果价格变为负,但价格通常为正)的值。

这个主要是为了抗操纵性。去中心化交易所(DEX)上的瞬时价格很容易被大额交易(如闪电贷攻击)瞬间操纵。TWAP 通过计算一段时间内的平均价格,大大增加了操纵价格的成本和难度。

最后就是更新储备量和时间戳,更新最后时间戳为当前区块时间。

具体计算TWAP的话:

  1. 在时间点 t1 读取 price0CumulativeLast 的值,记为 C1
  2. 在稍后的时间点 t2 再次读取 price0CumulativeLast 的值,记为 C2
  3. 计算时间差 T = t2 - t1(秒)
  4. 计算 TWAP:TWAP = (C2 - C1) / T
    • 这里的 (C2 - C1) 就是时间区间 T内的价格累计增量(即价格对时间的积分)。
    • 除以 T 就得到了该时间段内的平均价格。
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
    uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32);
    uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

    if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
        price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
        price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
    }

    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}

2.4 _mintFee()

这是 Uniswap V2 的协议手续费计算函数,负责在流动性增长时铸造额外的流动性代币作为协议收入。

首先从工厂合约获取协议手续费接收地址 feeTo,如果 feeTo 不是零地址,说明协议手续费功能已启用。

当手续费功能开启时计算手续费,然后铸造计算出的流动性代币给 feeTo 地址。

手续费功能开启时手续费计算方法这部分之前已经写过了,所以直接拷贝过来:

2.4.1 Uniswap V2 协议手续费 (PROTOCOL_FEE) 机制

2.4.1.1 项目方的需求
  • 在 Uniswap V1 中,所有手续费都归 LP 所有,协议开发者(Uniswap 团队)自身无法从中直接获利。
  • 为了激励协议的持续开发和维护,Uniswap V2 引入了协议手续费机制,允许项目方从协议中分得一杯羹。
2.4.1.2 phi (ϕ\phi) 参数
  • 定义ϕ\phi (phi) 代表项目方希望从总手续费中分走的比例。
    • 例如,如果项目方想分走 0.3% 手续费中的 1/6,那么 ϕ=1/6\phi = 1/6
    • 实际收取的协议费率为 0.3%×16=0.05%0.3\% \times \frac{1}{6} = 0.05\% (万分之五)。
2.4.1.3 实现方式:增发 Share
  • 核心思想:项目方通常不提供流动性,因此不能直接持有 LP Token 来分享收益。为了让项目方获得收益,Uniswap V2 采取了增发新的 LP Token (Share) 给项目方的方式。
  • “做大蛋糕,分增量”:项目方增发 Share 的逻辑是,在流动性池因手续费而变大(蛋糕变大)的同时,从这个“变大的部分”中分走一小块,而不是侵占现有 LP 的初始利益。
2.4.1.4 数学推导:SM 的计算

假设:

  • S1S_1T1 时刻 LP 持有的 LP Token 总量。
  • K1K_1T1 时刻池子的流动性常数。
  • K2K_2T2 时刻池子的流动性常数(因手续费积累而增长)。
  • SMSM:项目方在 T2 时刻应增发的新 LP Token 数量。
  • ϕ\phi:项目方希望分走的手续费比例。

协议手续费的计算基于以下公式:

SMS1+SM=ϕK2K1K2 \frac{SM}{S_1 + SM} = \phi \cdot \frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}}

这个公式的含义是:

  • 左侧 SMS1+SM\frac{SM}{S_1 + SM} 代表项目方增发的 Share 占总 Share 数量的比例。
  • 右侧 K2K1K2\frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}} 代表从 T1T2 时刻,因手续费积累而导致的流动性(L = sqrt(K))的增长比例。
  • ϕ\phi 是项目方从这个增长比例中分走的份额。

通过对上述公式进行推导,可以求得 SM 的表达式:

推导过程

  1. 初始公式: SMS1+SM=ϕK2K1K2 \frac{SM}{S_1 + SM} = \phi \cdot \frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}}
  2. 将右侧的 K2K1K2\frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}} 记为 FratioF_{ratio} (手续费导致的流动性增长比例)。 SMS1+SM=ϕFratio \frac{SM}{S_1 + SM} = \phi \cdot F_{ratio}
  3. 交叉相乘: SM=ϕFratio(S1+SM) SM = \phi \cdot F_{ratio} \cdot (S_1 + SM)
  4. 展开: SM=ϕFratioS1+ϕFratioSM SM = \phi \cdot F_{ratio} \cdot S_1 + \phi \cdot F_{ratio} \cdot SM
  5. 将包含 SMSM 的项移到等式左侧: SMϕFratioSM=ϕFratioS1 SM - \phi \cdot F_{ratio} \cdot SM = \phi \cdot F_{ratio} \cdot S_1
  6. 提取 SMSMSM(1ϕFratio)=ϕFratioS1 SM \cdot (1 - \phi \cdot F_{ratio}) = \phi \cdot F_{ratio} \cdot S_1
  7. 解出 SMSMSM=ϕFratio1ϕFratioS1 SM = \frac{\phi \cdot F_{ratio}}{1 - \phi \cdot F_{ratio}} \cdot S_1
  8. Fratio=K2K1K2F_{ratio} = \frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}} 代回: SM=ϕK2K1K21ϕK2K1K2S1 SM = \frac{\phi \cdot \frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}}}{1 - \phi \cdot \frac{\sqrt{K_2} - \sqrt{K_1}}{\sqrt{K_2}}} \cdot S_1
  9. 为了简化,分子分母同乘以 K2\sqrt{K_2}SM=ϕ(K2K1)K2ϕ(K2K1)S1 SM = \frac{\phi \cdot (\sqrt{K_2} - \sqrt{K_1})}{\sqrt{K_2} - \phi \cdot (\sqrt{K_2} - \sqrt{K_1})} \cdot S_1
  10. 进一步整理分母: K2ϕK2+ϕK1=(1ϕ)K2+ϕK1 \sqrt{K_2} - \phi\sqrt{K_2} + \phi\sqrt{K_1} = (1-\phi)\sqrt{K_2} + \phi\sqrt{K_1}
  11. 最终得到 SMSM 的表达式: SM=ϕ(K2K1)(1ϕ)K2+ϕK1S1 SM = \frac{\phi \cdot (\sqrt{K_2} - \sqrt{K_1})}{(1-\phi)\sqrt{K_2} + \phi\sqrt{K_1}} \cdot S_1
  12. 为了得到与白皮书和代码实现一致的形式,我们可以将分子分母同除以 ϕ\phiSM=ϕϕ(K2K1)(1ϕ)ϕK2+ϕϕK1S1 SM = \frac{\frac{\phi}{\phi} \cdot (\sqrt{K_2} - \sqrt{K_1})}{\frac{(1-\phi)}{\phi}\sqrt{K_2} + \frac{\phi}{\phi}\sqrt{K_1}} \cdot S_1 SM=K2K1(1ϕ1)K2+K1S1 SM = \frac{\sqrt{K_2} - \sqrt{K_1}}{(\frac{1}{\phi} - 1)\sqrt{K_2} + \sqrt{K_1}} \cdot S_1 当 Uniswap V2 协议费比例 ϕ=1/6\phi = 1/6 时,1ϕ1=11/61=61=5\frac{1}{\phi} - 1 = \frac{1}{1/6} - 1 = 6 - 1 = 5。 因此,最终用于计算的 SM 公式为: SM=K2K15K2+K1S1 SM = \frac{\sqrt{K_2} - \sqrt{K_1}}{5 \cdot \sqrt{K_2} + \sqrt{K_1}} \cdot S_1 这个公式与 Uniswap V2 白皮书和代码实现中的逻辑完全一致。

2.4.2 代码实现

function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (feeOn) {
        if (_kLast != 0) {
            uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
            uint rootKLast = Math.sqrt(_kLast);
            if (rootK > rootKLast) {
                uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                uint denominator = rootK.mul(5).add(rootKLast);
                uint liquidity = numerator / denominator;
                if (liquidity > 0) _mint(feeTo, liquidity);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}

2.5 mint()

这是添加流动性的核心函数,用户通过向交易对合约存入代币来获得流动性代币(LP Token),同时处理首次添加的特殊逻辑、协议手续费计算和状态更新。

同样的,先获取获取当前储备量和当前实际代币余额。然后获取当前合约实际持有的代币余额,这是当前池子真实持有的代币数量(包括用户刚刚转入的),因为用户在调用 mint 前已将代币 transfer 到 Pair 合约,所以:balance0 >= _reserve0, balance1 >= _reserve1

之后计算用户新增的代币数量,amount0amount1 就是用户本次提供的流动性,然后处理协议手续费。即计算应奖励给 feeTo 的额外 LP,向 feeTo 铸造这部分 LP并修改totalSupply

然后读取当前LP总供应量 totalSupply,这个读取的时机很重要,必须在_mintFee之后读取。

如果是第一次添加流动性,需要减去 MINIMUM_LIQUIDITY,目的如下:

  • 防止总供应量为 0(避免后续除零错误);
  • 防止第一个 LP 提供者移除全部流动性(因为 1000 LP 永远无法赎回);
  • 这是一种安全机制,不是“锁定流动性”,而是“永久销毁一小部分 LP”。

后续添加流动性的话就按照比例计算:用户应得的 LP 数量 = 其贡献 / 当前储备 × 总 LP,还有需要注意的一点是,取两个币种的最小值,这是为了确保不破坏 x * y = k

接下来就是验证铸造数量有效,铸造 LP 代币给接收者(_mint(to, liquidity)) 并更新池子状态(_update(balance0, balance1, _reserve0, _reserve1))

源码如下:

function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // gas savings
    uint balance0 = IERC20(token0).balanceOf(address(this));
    uint balance1 = IERC20(token1).balanceOf(address(this));
  
    uint amount0 = balance0.sub(_reserve0);
    uint amount1 = balance1.sub(_reserve1);
    
    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee

    if (_totalSupply == 0) {
        liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
        _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
    } else {
        liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
    }
    require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');

    _mint(to, liquidity);
    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Mint(msg.sender, amount0, amount1);
}

2.6 burn()

这个方法是用于移除流动性,看了添加流动性再看这个就很好理解了。

// 移除流动性
function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // gas savings
    address _token0 = token0; // gas savings
    address _token1 = token1; // gas savings
    // 获取当前合约持有的代币余额
    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    // 获取要赎回的 LP 数量,合约会烧毁这部分 LP,并按比例返还底层资产。
    uint liquidity = balanceOf[address(this)];
    bool feeOn = _mintFee(_reserve0, _reserve1);
    uint _totalSupply = totalSupply; // 注意时机
    // 计算应返还的代币数量(按比例)
    amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
    amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
    // 验证赎回金额有效
    require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
    _burn(address(this), liquidity); // 烧毁 LP 代币
    _safeTransfer(_token0, to, amount0); // 将代币转给用户指定地址
    _safeTransfer(_token1, to, amount1);
    // 重新获取转账后的余额(用于状态更新)
    balance0 = IERC20(_token0).balanceOf(address(this));
    balance1 = IERC20(_token1).balanceOf(address(this));
    //  更新储备和时间戳
    _update(balance0, balance1, _reserve0, _reserve1);
    if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
    emit Burn(msg.sender, amount0, amount1, to);
}

2.7 swap

swap 函数,用于执行 代币兑换(token swap)。是 Uniswap V2 的核心交易逻辑,实现了恒定乘积做市商模型(x * y = k)。

首先验证输出金额有效,至少要兑换出一种代币,不能两个都为 0。然后获取当前储备量,并验证池子有足够流动性,不能兑换超过当前储备的代币(必须留至少 1 wei,防止除零或池子枯竭),这里使用的是小于并不是小于等于,确保池子永不为空。

然后验证接收地址合法,禁止将代币转给 token0token1 合约本身。这是因为某些 ERC20 合约在收到代币时会执行逻辑(如销毁、回调),可能导致资金锁定或攻击。

之后使用_safeTransfer进行转账,先将用户要的代币提前转出,再验证用户是否支付了足够对价。这是“乐观”执行——假设用户会通过回调补足支付。如果后续验证失败,整个交易会 revert,转账也会回滚。

优点:支持闪电贷(Flash Swap)!用户可以先拿走代币,只要在回调中还回来(或支付对价)即可。

之后调用回调函数(支持闪电贷和复杂逻辑):

  • 如果 data 非空,说明 to 是一个合约,且实现了 uniswapV2Call 接口。
  • 调用该回调,允许 to 执行任意逻辑,例如:
    • 用借出的代币进行套利;
    • 支付兑换所需的另一种代币;
    • 执行组合策略。
  • 这是闪电贷和复杂 DeFi 组合的基础。

再之后获取转账+回调后的实际余额,此时池子余额 = 原储备 - 已转出 + 用户在回调中转入的代币,这些值将用于计算用户实际支付了多少(amount0In, amount1In

然后计算用户实际支付的输入金额(amount0In, amount1In),并验证验证用户至少支付了一种代币

在最后,Uniswap V2 收取 0.3% 交易费。手续费不立即提取,而是留在池子中,使 x * y 略微增长。为验证交易合法,需检查:扣除手续费后的余额仍满足 x * y >= k。这里为避免小数,两边同乘 1000 * 1000

最终更新池子状态,并触发 Swap 事件。

function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
    (uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // gas savings
    require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

    uint balance0;
    uint balance1;
    {
        // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
    }

    uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
    uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
    require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
    
    {
        // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        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),
            'UniswapV2: K'
        );
    }

    _update(balance0, balance1, _reserve0, _reserve1);
    emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}

3. 测试

这是一个添加和移除流动性的测试代码,只使用了UniswapV2Pair

const { ethers } = require("hardhat");

async function main() {
    console.log("[+] 开始 Uniswap V2 流动性管理测试:直接使用 Pair 合约");

    // ======================
    // 0. 准备账户
    // ======================
    const [deployer, user] = await ethers.getSigners();
    console.log("[+] 部署者账户:", deployer.address);
    console.log("[+] 用户账户:", user.address);
    console.log("[+] 部署者余额:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)), "ETH");

    // ======================
    // 1. 部署 Uniswap 核心合约
    // ======================
    console.log("\n[+] 部署 Uniswap V2 核心合约");

    // 部署 Factory
    const Factory = await ethers.getContractFactory(
        "contracts/uniswap-v2-core/contracts/UniswapV2Factory.sol:UniswapV2Factory"
    );
    const factory = await Factory.deploy(deployer.address);
    await factory.waitForDeployment();
    const FACTORY_ADDRESS = await factory.getAddress();
    console.log("[+] Factory 部署成功! 地址:", FACTORY_ADDRESS);

    // ======================
    // 2. 部署测试代币
    // ======================
    console.log("\n[+] 部署测试代币");

    const ERC20Contract = await ethers.getContractFactory(
        "contracts/uniswap-v2-periphery/contracts/test/ERC20.sol:ERC20"
    );

    const tokenA = await ERC20Contract.deploy(ethers.parseEther("10000"));
    await tokenA.waitForDeployment();
    const tokenAAddress = await tokenA.getAddress();
    console.log("[+] TokenA 部署成功! 地址:", tokenAAddress);

    const tokenB = await ERC20Contract.deploy(ethers.parseEther("1000000"));
    await tokenB.waitForDeployment();
    const tokenBAddress = await tokenB.getAddress();
    console.log("[+] TokenB 部署成功! 地址:", tokenBAddress);

    // ======================
    // 3. 创建交易对
    // ======================
    console.log("\n[+] 创建交易对");

    // 创建交易对
    await factory.createPair(tokenAAddress, tokenBAddress);
    const pairAddress = await factory.getPair(tokenAAddress, tokenBAddress);
    console.log("[+] 交易对创建成功! 地址:", pairAddress);

    // 获取交易对合约实例
    const pair = await ethers.getContractAt(
        "contracts/uniswap-v2-core/contracts/interfaces/IUniswapV2Pair.sol:IUniswapV2Pair",
        pairAddress
    );

    // 获取 token0 和 token1
    const token0 = await pair.token0();
    const token1 = await pair.token1();
    console.log(`[+] Token0: ${token0 === tokenAAddress ? "TokenA" : "TokenB"}`);
    console.log(`[+] Token1: ${token1 === tokenBAddress ? "TokenB" : "TokenA"}`);

    // 转账代币给用户用于添加流动性
    const tokenAAmount = ethers.parseEther("100");
    const tokenBAmount = ethers.parseEther("10000");
    await tokenA.transfer(user.address, tokenAAmount);
    await tokenB.transfer(user.address, tokenBAmount);

    // 记录初始代币余额
    const initialTokenABalance = await tokenA.balanceOf(user.address);
    const initialTokenBBalance = await tokenB.balanceOf(user.address);
    console.log("[+] 用户初始代币余额:");
    console.log(`   - TokenA: ${ethers.formatEther(initialTokenABalance)}`);
    console.log(`   - TokenB: ${ethers.formatEther(initialTokenBBalance)}`);

    // ======================
    // 4. 添加流动性(直接使用 Pair 合约)
    // ======================
    console.log("\n[+] 添加流动性(直接使用 Pair 合约)");

    // 用户授权 Pair 合约使用代币
    await tokenA.connect(user).approve(pairAddress, ethers.MaxUint256);
    await tokenB.connect(user).approve(pairAddress, ethers.MaxUint256);
    console.log("[+] 用户已授权 Pair 合约使用代币");

    // 获取添加前的储备量
    const reservesBefore = await pair.getReserves();
    console.log("[+] 添加前储备量:");
    console.log(`   - TokenA: ${ethers.formatEther(reservesBefore[0])}`);
    console.log(`   - TokenB: ${ethers.formatEther(reservesBefore[1])}`);

    // 用户向 Pair 合约转账代币
    await tokenA.connect(user).transfer(pairAddress, tokenAAmount);
    await tokenB.connect(user).transfer(pairAddress, tokenBAmount);
    console.log("[+] 用户已向 Pair 合约转账代币");

    // 记录转账后代币余额
    const afterTransferTokenABalance = await tokenA.balanceOf(user.address);
    const afterTransferTokenBBalance = await tokenB.balanceOf(user.address);
    console.log("[+] 转账后用户代币余额:");
    console.log(`   - TokenA: ${ethers.formatEther(afterTransferTokenABalance)}`);
    console.log(`   - TokenB: ${ethers.formatEther(afterTransferTokenBBalance)}`);
    console.log(`   - TokenA 减少: ${ethers.formatEther(initialTokenABalance - afterTransferTokenABalance)}`);
    console.log(`   - TokenB 减少: ${ethers.formatEther(initialTokenBBalance - afterTransferTokenBBalance)}`);

    // 调用 mint 函数添加流动性
    const mintTx = await pair.connect(user).mint(user.address);
    await mintTx.wait();
    console.log("[+] 流动性添加成功");

    // 获取用户流动性代币余额
    const liquidityBalance = await pair.balanceOf(user.address);
    console.log("[+] 用户流动性代币 (LP Token) 余额:", ethers.formatEther(liquidityBalance));

    // 获取添加后的储备量
    const reservesAfter = await pair.getReserves();
    console.log("[+] 添加后储备量:");
    console.log(`   - TokenA: ${ethers.formatEther(reservesAfter[0])}`);
    console.log(`   - TokenB: ${ethers.formatEther(reservesAfter[1])}`);

    // 记录添加流动性后代币余额
    const afterMintTokenABalance = await tokenA.balanceOf(user.address);
    const afterMintTokenBBalance = await tokenB.balanceOf(user.address);
    console.log("[+] 添加流动性后用户代币余额:");
    console.log(`   - TokenA: ${ethers.formatEther(afterMintTokenABalance)}`);
    console.log(`   - TokenB: ${ethers.formatEther(afterMintTokenBBalance)}`);
    console.log(`   - 总减少: TokenA ${ethers.formatEther(initialTokenABalance - afterMintTokenABalance)}, TokenB ${ethers.formatEther(initialTokenBBalance - afterMintTokenBBalance)}`);

    // ======================
    // 5. 移除流动性(直接使用 Pair 合约)
    // ======================
    console.log("\n[+] 移除流动性(直接使用 Pair 合约)");

    // 获取移除前的代币余额
    const userTokenABalanceBefore = await tokenA.balanceOf(user.address);
    const userTokenBBalanceBefore = await tokenB.balanceOf(user.address);

    // 用户授权 Pair 合约使用流动性代币
    await pair.connect(user).approve(pairAddress, liquidityBalance);
    console.log("[+] 用户已授权 Pair 合约使用流动性代币");

    // 用户将流动性代币转移到 Pair 合约
    console.log("[+] 用户将流动性代币转移到 Pair 合约");
    await pair.connect(user).transfer(pairAddress, liquidityBalance);

    // 调用 burn 函数移除流动性
    console.log("[+] 调用 burn 函数");
    const burnTx = await pair.connect(user).burn(user.address);
    await burnTx.wait();
    console.log("[+] 流动性移除成功");

    // 获取移除后的流动性代币余额
    const liquidityBalanceAfter = await pair.balanceOf(user.address);
    console.log("[+] 移除后流动性代币余额:", ethers.formatEther(liquidityBalanceAfter));

    // 获取移除后的代币余额
    const userTokenABalanceAfter = await tokenA.balanceOf(user.address);
    const userTokenBBalanceAfter = await tokenB.balanceOf(user.address);

    // 计算用户获得的代币数量
    const tokenAReceived = userTokenABalanceAfter - userTokenABalanceBefore;
    const tokenBReceived = userTokenBBalanceAfter - userTokenBBalanceBefore;

    console.log("\n[+] 移除流动性结果:");
    console.log(`   - 获得 TokenA: ${ethers.formatEther(tokenAReceived)}`);
    console.log(`   - 获得 TokenB: ${ethers.formatEther(tokenBReceived)}`);
    console.log(`   - 销毁流动性代币: ${ethers.formatEther(liquidityBalance)}`);

    // 获取移除后的储备量
    const reservesAfterRemove = await pair.getReserves();
    console.log("\n[+] 移除后储备量:");
    console.log(`   - TokenA: ${ethers.formatEther(reservesAfterRemove[0])}`);
    console.log(`   - TokenB: ${ethers.formatEther(reservesAfterRemove[1])}`);

    console.log("\n[+] Uniswap V2 流动性管理测试完成");
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error("[!] 操作失败:", error);
        process.exit(1);
    });

输出:

ghostasky@gaowentaodeMac-mini UniswapV2_SourceRead % npx hardhat run scripts/2.js --network localhost
[+] 开始 Uniswap V2 流动性管理测试:直接使用 Pair 合约
[+] 部署者账户: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
[+] 用户账户: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
[+] 部署者余额: 10000.0 ETH

[+] 部署 Uniswap V2 核心合约
[+] Factory 部署成功! 地址: 0x5FbDB2315678afecb367f032d93F642f64180aa3

[+] 部署测试代币
[+] TokenA 部署成功! 地址: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
[+] TokenB 部署成功! 地址: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

[+] 创建交易对
[+] 交易对创建成功! 地址: 0x581Caa5c335A3367D59939547166BdA139561e84
[+] Token0: TokenB
[+] Token1: TokenA
[+] 用户初始代币余额:
   - TokenA: 100.0
   - TokenB: 10000.0

[+] 添加流动性(直接使用 Pair 合约)
[+] 用户已授权 Pair 合约使用代币
[+] 添加前储备量:
   - TokenA: 0.0
   - TokenB: 0.0
[+] 用户已向 Pair 合约转账代币
[+] 转账后用户代币余额:
   - TokenA: 0.0
   - TokenB: 0.0
   - TokenA 减少: 100.0
   - TokenB 减少: 10000.0
[+] 流动性添加成功
[+] 用户流动性代币 (LP Token) 余额: 999.999999999999999
[+] 添加后储备量:
   - TokenA: 10000.0
   - TokenB: 100.0
[+] 添加流动性后用户代币余额:
   - TokenA: 0.0
   - TokenB: 0.0
   - 总减少: TokenA 100.0, TokenB 10000.0

[+] 移除流动性(直接使用 Pair 合约)
[+] 用户已授权 Pair 合约使用流动性代币
[+] 用户将流动性代币转移到 Pair 合约
[+] 调用 burn 函数
[+] 流动性移除成功
[+] 移除后流动性代币余额: 0.0

[+] 移除流动性结果:
   - 获得 TokenA: 99.9999999999999999
   - 获得 TokenB: 9999.99999999999999
   - 销毁流动性代币: 999.999999999999999

[+] 移除后储备量:
   - TokenA: 0.00000000000001
   - TokenB: 0.0000000000000001

[+] Uniswap V2 流动性管理测试完成

目录