UniswapV1 自学系列04:增加流动性 (Adding Liquidity)
在前面的章节中,我们实现了基础的 addLiquidity 函数,但存在一个重大问题:它允许用户以任意比例添加流动性,这会严重影响交易价格的稳定性。
UniswapV1 自学系列04:增加流动性 (Adding Liquidity)
本系列文章将带您从零构建一个 UniswapV1 去中心化交易所,深入理解 AMM(自动做市商)机制的核心原理。
1. 流动性管理的重要性
在前面的章节中,我们实现了基础的 addLiquidity 函数,但存在一个重大问题:它允许用户以任意比例添加流动性,这会严重影响交易价格的稳定性。
1.1 现有实现的问题
目前的 addLiquidity 函数实现如下:
function addLiquidity(uint256 _tokenAmount) public payable {
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), _tokenAmount);
}
核心问题:该函数允许用户随时以任意比例添加流动性,这会破坏价格机制。
2. 价格机制原理回顾
2.1 汇率计算公式
在 AMM 机制中,汇率由储备比例决定:
P_ETH = tokenReserve / ethReserve
P_TOKEN = ethReserve / tokenReserve
其中:
P_ETH和P_TOKEN分别是 ETH 和代币的价格ethReserve和tokenReserve分别是 ETH 和代币的储备量
2.2 价格稳定的重要性
价格稳定机制确保:
- 防止价格操纵:恶意用户无法通过不当的流动性比例操纵市场价格
- 维持价格预言机功能:去中心化交易所能够作为可靠的价格参考
- 保护套利者利益:价格与中心化交易所保持一致,减少无效套利
- 用户体验保障:确保交易价格符合市场预期
3. 完善的流动性添加机制
3.1 双分支设计思路
改进后的 addLiquidity 函数需要处理两种场景:
- 池子初始化:首次添加流动性时允许任意比例
- 追加流动性:后续添加必须严格按照现有储备比例
3.2 完整实现代码
/**
* @dev 向流动性池添加资金(完善版本)
* @param _tokenAmount 用户提供的代币数量上限
* @notice 需要同时发送 ETH 和代币,追加流动性时比例必须匹配当前储备比例
*/
function addLiquidity(uint256 _tokenAmount) public payable {
if (getReserve() == 0) {
// 分支1:初始化流动性池 - 允许任意比例
// 这是设定初始价格的关键时刻
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), _tokenAmount);
} else {
// 分支2:追加流动性 - 必须维持现有比例
uint256 ethReserve = address(this).balance - msg.value;
uint256 tokenReserve = getReserve();
// 基于用户提供的 ETH 数量计算所需的代币数量
// tokenAmount = (msg.value × tokenReserve) ÷ ethReserve
uint256 tokenAmount = (msg.value * tokenReserve) / ethReserve;
// 确保用户提供了足够的代币
require(_tokenAmount >= tokenAmount, "insufficient token amount");
// 只转入计算得出的精确代币数量,多余部分不使用
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), tokenAmount);
}
}
3.3 关键设计要点详解
3.3.1 储备量计算
uint256 ethReserve = address(this).balance - msg.value;
重要细节:需要从当前余额中减去 msg.value,因为在 payable 函数中,发送的 ETH 已经被加入合约余额。
3.3.2 比例计算公式
uint256 tokenAmount = (msg.value * tokenReserve) / ethReserve;
数学原理:
- 当前比例:
tokenReserve : ethReserve - 新增 ETH:
msg.value - 所需代币:
msg.value × (tokenReserve / ethReserve)
3.3.3 安全检查机制
require(_tokenAmount >= tokenAmount, "insufficient token amount");
保护作用:确保用户提供了足够的代币,防止因代币不足导致的交易失败。
4. 使用 Foundry 测试流动性功能
4.1 测试环境准备
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/Exchange.sol";
import "../src/Token.sol";
contract LiquidityTest is Test {
Exchange exchange;
Token token;
address user = makeAddr("user");
address user2 = makeAddr("user2");
function setUp() public {
token = new Token("Test Token", "TEST", 1000000 ether);
exchange = new Exchange(address(token));
// 为测试用户分配足够的代币和 ETH
token.transfer(user, 5000 ether);
token.transfer(user2, 5000 ether);
vm.deal(user, 3000 ether);
vm.deal(user2, 3000 ether);
}
}
4.2 初始流动性添加测试
function testInitialLiquidity() public {
vm.startPrank(user);
// 1. 添加初始流动性:任意比例
token.approve(address(exchange), 1000 ether);
exchange.addLiquidity{value: 500 ether}(1000 ether);
// 2. 验证储备状态
assertEq(address(exchange).balance, 500 ether, "ETH reserve incorrect");
assertEq(exchange.getReserve(), 1000 ether, "Token reserve incorrect");
// 3. 验证初始价格设定
// 价格比例:1000 Token : 500 ETH = 2:1
uint256 expectedPrice = exchange.getPrice(1000 ether, 500 ether);
assertEq(expectedPrice, 2000, "Initial price incorrect"); // 2.0 with precision
vm.stopPrank();
}
4.3 比例化流动性添加测试
function testProportionalLiquidity() public {
// 第一个用户添加初始流动性
vm.startPrank(user);
token.approve(address(exchange), 1000 ether);
exchange.addLiquidity{value: 500 ether}(1000 ether);
vm.stopPrank();
// 第二个用户按比例添加流动性
vm.startPrank(user2);
// 当前比例:1000 Token : 500 ETH = 2:1
// 如果添加 100 ETH,应该需要 200 Token
token.approve(address(exchange), 250 ether); // 提供足够的授权
exchange.addLiquidity{value: 100 ether}(250 ether);
// 验证比例添加成功
assertEq(address(exchange).balance, 600 ether, "Total ETH incorrect");
assertEq(exchange.getReserve(), 1200 ether, "Total tokens incorrect");
// 验证价格比例不变
uint256 newPrice = exchange.getPrice(1200 ether, 600 ether);
assertEq(newPrice, 2000, "Price ratio changed"); // 仍然是 2:1
vm.stopPrank();
}
4.4 代币不足错误测试
function testInsufficientTokens() public {
// 添加初始流动性
vm.startPrank(user);
token.approve(address(exchange), 1000 ether);
exchange.addLiquidity{value: 500 ether}(1000 ether);
vm.stopPrank();
// 尝试添加流动性但代币不足
vm.startPrank(user2);
// 当前比例需要 200 Token,但只提供 150 Token
token.approve(address(exchange), 150 ether);
vm.expectRevert("insufficient token amount");
exchange.addLiquidity{value: 100 ether}(150 ether);
vm.stopPrank();
}
4.5 精确代币数量验证测试
function testExactTokenUsage() public {
// 添加初始流动性
vm.startPrank(user);
token.approve(address(exchange), 2000 ether);
exchange.addLiquidity{value: 1000 ether}(2000 ether);
vm.stopPrank();
// 记录用户2的初始余额
vm.startPrank(user2);
uint256 initialTokenBalance = token.balanceOf(user2);
// 提供多余的代币授权,但只应使用精确数量
token.approve(address(exchange), 500 ether); // 授权500,但只需要200
exchange.addLiquidity{value: 100 ether}(500 ether);
// 验证只使用了精确的代币数量
uint256 finalTokenBalance = token.balanceOf(user2);
uint256 tokensUsed = initialTokenBalance - finalTokenBalance;
assertEq(tokensUsed, 200 ether, "Should use exact token amount");
vm.stopPrank();
}
4.6 运行测试命令
# 运行所有流动性相关测试
forge test --match-test "testInitialLiquidity|testProportionalLiquidity|testInsufficientTokens|testExactTokenUsage" -v
# 查看详细的测试输出
forge test --match-test testProportionalLiquidity -vvv
5. 流动性提供者的经济激励
5.1 流动性代币机制(LP Token)
在真实的 UniswapV1 实现中,流动性提供者会获得 LP(Liquidity Provider)代币作为凭证:
// 注意:这是概念性代码,实际实现需要 ERC20 标准
mapping(address => uint256) public liquidityBalances;
function addLiquidity(uint256 _tokenAmount) public payable {
if (totalLiquidity == 0) {
// 初始流动性:LP代币 = sqrt(ETH * Token)
uint256 liquidity = sqrt(msg.value * _tokenAmount);
liquidityBalances[msg.sender] = liquidity;
totalLiquidity = liquidity;
} else {
// 按比例分配:LP代币 = (新增ETH / 总ETH) * 总LP代币
uint256 liquidity = (msg.value * totalLiquidity) / address(this).balance;
liquidityBalances[msg.sender] += liquidity;
totalLiquidity += liquidity;
}
}
5.2 收益分配机制
流动性提供者通过以下方式获得收益:
- 交易手续费分成:每笔交易的 0.3% 手续费按 LP 代币比例分配
- 价格波动收益:当价格回归时,流动性提供者获得额外收益
- 激励代币奖励:协议可能提供额外的治理代币奖励
6. 高级优化和注意事项
6.1 精度处理
在实际实现中需要注意 Solidity 的整数除法精度问题:
// 使用更高的精度来避免舍入误差
uint256 constant PRECISION = 1e18;
uint256 tokenAmount = (msg.value * tokenReserve * PRECISION) / (ethReserve * PRECISION);
6.2 最小流动性保护
uint256 constant MINIMUM_LIQUIDITY = 1000;
function addLiquidity(uint256 _tokenAmount) public payable {
require(msg.value > MINIMUM_LIQUIDITY, "Insufficient ETH amount");
require(_tokenAmount > MINIMUM_LIQUIDITY, "Insufficient token amount");
// ... 其他逻辑
}
6.3 重入攻击防护
modifier nonReentrant() {
require(!locked, "ReentrancyGuard: reentrant call");
locked = true;
_;
locked = false;
}
function addLiquidity(uint256 _tokenAmount) public payable nonReentrant {
// ... 函数逻辑
}
7. 小结
本章完善了 UniswapV1 的流动性管理机制:
- 双分支设计:区分初始化和追加流动性场景
- 比例保护机制:确保价格稳定性不被破坏
- 精确计算:只使用必需的代币数量,避免浪费
- 安全检查:充分的边界条件验证
- 完整测试覆盖:使用 Foundry 框架进行全面测试
通过这些改进,我们的去中心化交易所获得了稳定可靠的流动性管理能力,为后续的高级功能奠定了坚实基础。流动性提供者可以安全地参与到 AMM 生态系统中,享受去中心化金融带来的收益机会。
? 项目仓库
完整项目代码请访问: https://github.com/RyanWeb31110/uniswapv1_tech
本系列文章是基于该项目的完整教学实现,欢迎克隆代码进行实践学习!