本文是 UniswapV2 深入解析系列的第二篇文章,重点讲解流动性池的工作原理和 LP 代币的铸造机制。
本文是 UniswapV2 深入解析系列的第二篇文章,重点讲解流动性池的工作原理和 LP 代币的铸造机制。
没有流动性就无法进行交易,因此我们需要实现的第一个核心功能就是流动性池。流动性池本质上是一个智能合约,它存储代币流动性并允许基于这些流动性进行代币交换。
"汇集流动性"的过程就是将代币发送到智能合约中并存储一定时间的过程。
用户通过提供流动性获得对应的 LP(流动性提供者)代币作为凭证和奖励。
虽然每个合约都有自己的存储空间,ERC20 代币通过 mapping 记录地址和余额的对应关系,但仅仅依赖 ERC20 合约中的余额来管理流动性是不够的,主要原因包括:
价格操纵风险:如果只依赖 ERC20 余额,攻击者可能会向池子发送大量代币,进行有利的交换,然后套现离场。
更新控制需求:我们需要精确控制储备金何时更新,确保系统的安全性和一致性。
为了避免价格操纵和确保系统安全,我们需要在合约层面独立跟踪池子的储备金。我们使用 reserve0
和 reserve1
变量来跟踪两种代币的储备量:
/**
* @title UniswapV2Pair 核心交易对合约
* @notice 管理特定代币对的流动性和交易
*/
contract ZuniswapV2Pair is ERC20, Math {
// 代币0的储备量
uint256 private reserve0;
// 代币1的储备量
uint256 private reserve1;
// ... 其他代码省略,完整代码请查看项目仓库
}
设计要点:
在 Uniswap V2 中,流动性管理被简化为 LP 代币管理:
这种设计使得核心合约专注于底层操作,而复杂的用户交互逻辑由外围合约处理。
下面是用于存入新流动性的底层函数:
/**
* @notice 铸造 LP 代币,添加流动性到池子
* @dev 调用前需要先将代币转账到合约地址
* @return liquidity 铸造的 LP 代币数量
*/
function mint() public {
// 获取当前合约在两种代币中的余额
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
// 计算新增的代币数量(当前余额减去储备金)
uint256 amount0 = balance0 - reserve0;
uint256 amount1 = balance1 - reserve1;
uint256 liquidity;
// 初始流动性提供时的处理
if (totalSupply == 0) {
// 使用几何平均数计算初始 LP 代币数量
liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
// 永久锁定最小流动性以防止攻击
_mint(address(0), MINIMUM_LIQUIDITY);
} else {
// 后续流动性添加时的处理
// 取较小值以惩罚不平衡的流动性提供
liquidity = Math.min(
(amount0 * totalSupply) / reserve0,
(amount1 * totalSupply) / reserve1
);
}
// 检查是否有足够的流动性可以铸造
if (liquidity <= 0) revert InsufficientLiquidityMinted();
// 向用户铸造 LP 代币
_mint(msg.sender, liquidity);
// 更新储备金数量
_update(balance0, balance1);
// 发出添加流动性事件
emit Mint(msg.sender, amount0, amount1);
}
函数流程解析:
当池子中没有流动性时(totalSupply == 0
),我们需要确定应该铸造多少 LP 代币。Uniswap V2 选择使用几何平均数的原因包括:
公式:
初始LP代币数量 = sqrt(amount0 × amount1)
优势分析:
// 最小流动性常量(1000 wei)
uint256 public constant MINIMUM_LIQUIDITY = 1000;
if (totalSupply == 0) {
liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY);
}
作用机制:
防攻击:防止恶意用户让单个池子代币份额(1 wei)变得过于昂贵
数学原理解析:
让我们通过具体计算来理解这个10万美元是怎么来的:
假设攻击者创建一个新的流动性池,想让每个LP代币价值100美元:
攻击者的操作:
sqrt(X × X) = X
个X - 1000
个LP代币价格目标达成的条件:
X × 100美元
1000 × 100 = 10万美元
攻击成本:
实际例子:
sqrt(50万 × 50万) = 50万个
50万 - 1000 = 499,000个
LP代币这种设计确保了攻击的成本远远超过可能的收益,从经济角度让攻击变得毫无意义。
当池子已有流动性时,新的 LP 代币数量必须满足两个核心要求:
回想一下在 Uniswap V1 中,由于只有一个代币对(ETH),计算相对简单:
铸造的流动性 = LP代币总供应量 × (存入数量 / 储备金)
这个公式清晰明了,因为只需要考虑一种代币的比例关系。
在 Uniswap V2 中,情况变得复杂了,因为现在有两种底层代币。我们需要回答一个关键问题:应该使用哪种代币来计算 LP 代币数量?
基本公式(继承自V1):
新增LP代币 = 已发行总量 × (存入数量 / 现有储备)
由于有两种代币,我们需要分别计算:
liquidity0 = (amount0 * totalSupply) / reserve0; // 基于代币0计算
liquidity1 = (amount1 * totalSupply) / reserve1; // 基于代币1计算
这里出现了一个有趣的数学规律:
当存入比例接近储备比例时:
liquidity0
和 liquidity1
的值非常接近当存入比例偏离储备比例时:
liquidity0
和 liquidity1
的值会产生显著差异面对两个不同的计算结果,我们有两个选择:
// 错误的做法
liquidity = Math.max(liquidity0, liquidity1);
问题分析:
// 正确的做法
liquidity = Math.min(liquidity0, liquidity1);
优势分析:
平衡流动性提供:
不平衡流动性提供:
这种设计巧妙地将不平衡的成本转嫁给了流动性提供者,而不是整个池子,从而维护了系统的稳定性。
使用 Foundry 进行智能合约测试的优势:
/**
* @title UniswapV2Pair 测试合约
* @notice 使用 Foundry 框架测试交易对合约功能
*/
contract ZuniswapV2PairTest is Test {
// 测试用的 ERC20 代币
ERC20Mintable token0;
ERC20Mintable token1;
// 被测试的交易对合约
ZuniswapV2Pair pair;
/**
* @notice 测试环境初始化
* @dev 每个测试函数执行前都会调用此函数
*/
function setUp() public {
// 创建两个测试代币
token0 = new ERC20Mintable("Token A", "TKNA");
token1 = new ERC20Mintable("Token B", "TKNB");
// 创建交易对合约
pair = new ZuniswapV2Pair(address(token0), address(token1));
// 为测试合约铸造代币
token0.mint(10 ether);
token1.mint(10 ether);
}
}
/**
* @notice 测试初始流动性提供(引导池子)
*/
function testMintBootstrap() public {
// 向交易对转入初始流动性
token0.transfer(address(pair), 1 ether);
token1.transfer(address(pair), 1 ether);
// 调用 mint 函数铸造 LP 代币
pair.mint();
// 验证 LP 代币余额(扣除最小流动性)
assertEq(pair.balanceOf(address(this)), 1 ether - 1000);
// 验证储备金更新
assertReserves(1 ether, 1 ether);
// 验证总供应量
assertEq(pair.totalSupply(), 1 ether);
}
测试要点:
/**
* @notice 测试向已有流动性的池子添加平衡流动性
*/
function testMintWhenTheresLiquidity() public {
// 第一次添加流动性
token0.transfer(address(pair), 1 ether);
token1.transfer(address(pair), 1 ether);
pair.mint(); // 获得 1 LP 代币(减去最小流动性)
// 第二次添加流动性(平衡添加)
token0.transfer(address(pair), 2 ether);
token1.transfer(address(pair), 2 ether);
pair.mint(); // 获得 2 LP 代币
// 验证最终状态
assertEq(pair.balanceOf(address(this)), 3 ether - 1000);
assertEq(pair.totalSupply(), 3 ether);
assertReserves(3 ether, 3 ether);
}
/**
* @notice 测试不平衡流动性提供的惩罚机制
*/
function testMintUnbalanced() public {
// 初始流动性
token0.transfer(address(pair), 1 ether);
token1.transfer(address(pair), 1 ether);
pair.mint();
assertEq(pair.balanceOf(address(this)), 1 ether - 1000);
assertReserves(1 ether, 1 ether);
// 不平衡流动性提供(token0 更多)
token0.transfer(address(pair), 2 ether);
token1.transfer(address(pair), 1 ether);
pair.mint();
// 验证惩罚效果:虽然提供了更多 token0,仍只获得 1 LP 代币
assertEq(pair.balanceOf(address(this)), 2 ether - 1000);
assertReserves(3 ether, 2 ether);
}
关键测试点:
/**
* @notice 验证储备金数量的辅助函数
* @param expectedReserve0 期望的 token0 储备金
* @param expectedReserve1 期望的 token1 储备金
*/
function assertReserves(uint256 expectedReserve0, uint256 expectedReserve1) internal {
(uint256 reserve0, uint256 reserve1,) = pair.getReserves();
assertEq(reserve0, expectedReserve0);
assertEq(reserve1, expectedReserve1);
}
# 运行所有测试
forge test
# 运行特定测试并显示详细输出
forge test --match-test testMintBootstrap -vvv
# 生成测试覆盖率报告
forge coverage
# 生成 Gas 使用快照
forge snapshot
安全第一
数学稳定性
模块化设计
流动性计算
sqrt(amount0 × amount1) - MINIMUM_LIQUIDITY
min(amount0 × totalSupply / reserve0, amount1 × totalSupply / reserve1)
安全机制
测试策略
流动性提供者
开发者
审计要点
通过本文的学习,我们深入理解了 UniswapV2 流动性池的核心机制和 LP 代币铸造的实现细节。这为后续理解交易机制和高级功能奠定了坚实的基础。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!