UniswapV1 自学系列04:增加流动性 (Adding Liquidity)

  • 青山
  • 发布于 3天前
  • 阅读 78

在前面的章节中,我们实现了基础的 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_ETHP_TOKEN 分别是 ETH 和代币的价格
  • ethReservetokenReserve 分别是 ETH 和代币的储备量

2.2 价格稳定的重要性

价格稳定机制确保:

  1. 防止价格操纵:恶意用户无法通过不当的流动性比例操纵市场价格
  2. 维持价格预言机功能:去中心化交易所能够作为可靠的价格参考
  3. 保护套利者利益:价格与中心化交易所保持一致,减少无效套利
  4. 用户体验保障:确保交易价格符合市场预期

3. 完善的流动性添加机制

3.1 双分支设计思路

改进后的 addLiquidity 函数需要处理两种场景:

  1. 池子初始化:首次添加流动性时允许任意比例
  2. 追加流动性:后续添加必须严格按照现有储备比例

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 收益分配机制

流动性提供者通过以下方式获得收益:

  1. 交易手续费分成:每笔交易的 0.3% 手续费按 LP 代币比例分配
  2. 价格波动收益:当价格回归时,流动性提供者获得额外收益
  3. 激励代币奖励:协议可能提供额外的治理代币奖励

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 的流动性管理机制:

  1. 双分支设计:区分初始化和追加流动性场景
  2. 比例保护机制:确保价格稳定性不被破坏
  3. 精确计算:只使用必需的代币数量,避免浪费
  4. 安全检查:充分的边界条件验证
  5. 完整测试覆盖:使用 Foundry 框架进行全面测试

通过这些改进,我们的去中心化交易所获得了稳定可靠的流动性管理能力,为后续的高级功能奠定了坚实基础。流动性提供者可以安全地参与到 AMM 生态系统中,享受去中心化金融带来的收益机会。


📚 项目仓库

完整项目代码请访问: https://github.com/RyanWeb31110/uniswapv1_tech

本系列文章是基于该项目的完整教学实现,欢迎克隆代码进行实践学习!

  • 原创
  • 学分: 4
  • 分类: Uniswap
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
青山
青山
江湖只有他的大名,没有他的介绍。