UniswapV2源码阅读
这系列文章准备详细看看Uniswap的代码,使用的是 Hardhat
零。主要内容
当然!阅读 Uniswap V2 源码是一个非常棒的主题,可以写出一个非常深入和受欢迎的系列。基于第一篇《环境搭建》,后续文章可以按照从核心到外围、从基础到复杂的逻辑展开。
以下是一个可行的系列文章大纲,每一篇都聚焦于一个可验证、可操作的具体模块或功能:
UniswapV2 源码阅读系列文章大纲
第一篇:UniswapV2源码阅读——(一)环境搭建
(已完成)
- 内容:如何克隆代码、安装依赖、配置 Hardhat 本地开发环境、运行测试节点和基础脚本。
- 目标:让读者拥有一个可以编译、部署和交互的本地实验环境。
第二篇:UniswapV2源码阅读——(二)工厂合约(UniswapV2Factory)
- 核心内容:
- 剖析
UniswapV2Factory.sol:构造函数、状态变量(feeTo,feeToSetter,getPair映射)。 - 深入
createPair函数:如何通过bytescode和create2操作码确定性地创建配对合约。 - 角色权限:
feeToSetter的权限和作用(为协议收费设置接收地址)。
- 剖析
- 可操作实践:
- 写一个脚本,使用 Factory 合约在本地网络上创建一个新的交易对(如 WETH/USDT)。
- 验证
getPair映射是否正确更新。 - 查询创建的 Pair 合约地址。
第三篇:UniswapV2源码阅读——(三)配对合约核心(UniswapV2Pair)
- 核心内容:
- 合约结构:状态变量(
reserve0,reserve1,blockTimestampLast,price0CumulativeLast)。 - 核心功能:
mint(添加流动性),burn(移除流动性),swap(交易)。 - 深入理解
_update函数:如何更新准备金并累积价格。
- 合约结构:状态变量(
- 可操作实践:
- 在上一篇创建的 Pair 合约中,模拟执行
mint和swap操作。 - 编写脚本查询交易前后的储备金变化,直观理解 AMM 做市。
- 在上一篇创建的 Pair 合约中,模拟执行
第四篇:UniswapV2源码阅读——(四)价格预言机
- 核心内容:
- 原理阐述:如何通过“累积价格”提供防操纵的链上价格数据。
- 代码分析:
price0CumulativeLast和price1CumulativeLast的计算方式。 - 精度与时间窗口:讲解
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 项目
- 创建项目文件夹并进入
mkdir UniswapV2_SourceRead
cd UniswapV2_SourceRead- 初始化 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 / reserve0和reserve0 / reserve1) 乘以时间间隔来更新累积价格 (price0CumulativeLast和price1CumulativeLast)。这确保了累积价格反映了价格随时间的变化,但更新依赖于链上操作的发生。 - 闪电贷: 通过
swap的data参数回调实现,用户可在单笔交易中借出并归还资产。
双重身份:它本身也是一个符合 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 合约已能完成所有交易操作,但路由合约将这些操作整合,极大地方便了用户和前端应用。
核心功能:
- 流动性管理: 提供
addLiquidity和removeLiquidity等方法,支持 ETH/WETH 自动转换。 - 多跳兑换:如
swapExactTokensForTokens,支持路径[A, B, C]实现 A→B→C 兑换。 - 安全特性: 强制用户指定
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 交互流程如下:
-
创建交易对(按需)
- 用户不直接调用
Factory.createPair,而是通过 Router(如addLiquidity)间接触发。 - Router 内部检查 Pair 是否存在:若不存在,则调用
Factory.createPair(tokenA, tokenB)。 - Factory 仅在 Pair 不存在时,使用
CREATE2部署新 Pair 合约,其地址由排序后的代币地址和 init code 哈希唯一确定。
- 用户不直接调用
-
添加流动性
- 用户调用
Router.addLiquidity(或addLiquidityETH),传入两种代币(或 ETH)及数量,并授权代币给 Router。 - Router 将用户资产(代币或 WETH)转入 Pair 合约。
- Router 调用
Pair.mint(userAddress),Pair 直接向用户地址铸造 LP 代币,不经过 Router 中转。
- 用户调用
-
执行兑换
- 用户调用
Router.swapExactTokensForTokens等函数,指定兑换路径、输入量、最小输出量(amountOutMin)和截止时间(deadline)。 - Router 按路径依次调用各 Pair 的
swap(amount0Out, amount1Out, userAddress, "")。 - 每个 Pair 直接将输出代币转账至用户指定地址(通常为用户自身),完成多跳兑换。
- 用户调用
-
移除流动性
- 用户调用
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 coregraph 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
五、关键设计思想
-
安全边界控制
- 核心合约功能最小化(逻辑简洁,无外部依赖)
- 用户资金仅由核心合约持有,外围合约无资产所有权
-
确定性地址生成
- 通通过 CREATE2 部署 Pair 合约,确保地址可预先计算。
- 地址由工厂地址、init code 哈希及排序后代币地址的哈希(作为 salt)共同确定,无需链上查询即可推导 Pair 地址。
-
协议可扩展性
- 协议费开关:通过
feeTo激活协议收入(0.05%) - 外围合约可独立升级(如部署新 Router)
- 协议费开关:通过
-
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]:快速查找token0和token1对应的交易对地址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:要创建交易对的两种代币地址
- 返回值:新创建的交易对合约地址
- 整体流程:
- 校验
tokenA != tokenB - 排序
token0 < token1,避免重复创建 - 使用
create2预计算交易对地址 - 初始化交易对合约
- 更新
getPair映射和allPairs列表 - 触发
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: 1n3.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 / reserve0和reserve0 / reserve1) 乘以时间间隔来更新累积价格 (price0CumulativeLast和price1CumulativeLast)。这确保了累积价格反映了价格随时间的变化,但更新依赖于链上操作的发生。 - 闪电贷: 通过
swap的data参数回调实现,用户可在单笔交易中借出并归还资产。
双重身份:它本身也是一个符合 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)个会被销毁(mint给address(0))。SELECTOR: 用于在swap函数中安全调用代币的transfer方法。swap支持“先转出代币,再由接收方回调补足”的闪电贷模式。为避免调用恶意合约的transfer,Uniswap V2 通过SELECTOR确保只调用标准 ERC20 的transfer函数factory: 记录创建该 Pair 的工厂合约地址(即 UniswapV2Factory)token0,token1: 组成该交易对的两个 ERC20 代币地址reserve0,reserve1: 分别存储token0和token1的当前储备量blockTimestampLast: 记录上次更新储备的时间戳price0CumulativeLast,price1CumulativeLast: 分别累计token1/token0和token0/token1的价格积分值(即价格对时间的积分)。供外部调用者(如预言机)通过(currentCumulative - lastCumulative) / timeElapsed计算 TWAPkLast: 记录上次收取协议手续费时的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 的累积价格, 即price0CumulativeLast和price0CumulativeLast。其中,UQ112x112.encode(x) 表示将 uint112 转换为 Q112x112 定点数。.uqdiv(x)表示无溢出除法
对时间进行加权(即* timeElapsed),相当于计算了从上次更新到当前时刻这段时间内,价格对时间的“积分”。它代表了在这段时间内,价格曲线下的“面积”,然后进行累加。也就是在计算TWAP。
price0CumulativeLast因此成为了一个 从某个起始时间点开始,token0 相对于 token1 的价格对时间的累计积分。它是一个随时间单调递增(或递减,如果价格变为负,但价格通常为正)的值。
这个主要是为了抗操纵性。去中心化交易所(DEX)上的瞬时价格很容易被大额交易(如闪电贷攻击)瞬间操纵。TWAP 通过计算一段时间内的平均价格,大大增加了操纵价格的成本和难度。
最后就是更新储备量和时间戳,更新最后时间戳为当前区块时间。
具体计算TWAP的话:
- 在时间点
t1读取price0CumulativeLast的值,记为C1。 - 在稍后的时间点
t2再次读取price0CumulativeLast的值,记为C2。 - 计算时间差
T = t2 - t1(秒)。 - 计算 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) 代表项目方希望从总手续费中分走的比例。
- 例如,如果项目方想分走 0.3% 手续费中的 1/6,那么 。
- 实际收取的协议费率为 (万分之五)。
2.4.1.3 实现方式:增发 Share
- 核心思想:项目方通常不提供流动性,因此不能直接持有 LP Token 来分享收益。为了让项目方获得收益,Uniswap V2 采取了增发新的 LP Token (Share) 给项目方的方式。
- “做大蛋糕,分增量”:项目方增发 Share 的逻辑是,在流动性池因手续费而变大(蛋糕变大)的同时,从这个“变大的部分”中分走一小块,而不是侵占现有 LP 的初始利益。
2.4.1.4 数学推导:SM 的计算
假设:
- :
T1时刻 LP 持有的 LP Token 总量。 - :
T1时刻池子的流动性常数。 - :
T2时刻池子的流动性常数(因手续费积累而增长)。 - :项目方在
T2时刻应增发的新 LP Token 数量。 - :项目方希望分走的手续费比例。
协议手续费的计算基于以下公式:
这个公式的含义是:
- 左侧 代表项目方增发的 Share 占总 Share 数量的比例。
- 右侧 代表从
T1到T2时刻,因手续费积累而导致的流动性(L = sqrt(K))的增长比例。 - 是项目方从这个增长比例中分走的份额。
通过对上述公式进行推导,可以求得 SM 的表达式:
推导过程:
- 初始公式:
- 将右侧的 记为 (手续费导致的流动性增长比例)。
- 交叉相乘:
- 展开:
- 将包含 的项移到等式左侧:
- 提取 :
- 解出 :
- 将 代回:
- 为了简化,分子分母同乘以 :
- 进一步整理分母:
- 最终得到 的表达式:
- 为了得到与白皮书和代码实现一致的形式,我们可以将分子分母同除以 :
当 Uniswap V2 协议费比例 时,。
因此,最终用于计算的
SM公式为: 这个公式与 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。
之后计算用户新增的代币数量,amount0 和 amount1 就是用户本次提供的流动性,然后处理协议手续费。即计算应奖励给 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,防止除零或池子枯竭),这里使用的是小于并不是小于等于,确保池子永不为空。
然后验证接收地址合法,禁止将代币转给 token0 或 token1 合约本身。这是因为某些 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 流动性管理测试完成