2. 流动性添加

任务目标

在自动化做市商(AMM)合约中实现添加流动性功能:用户存入两种代币,合约计算应铸造的LP代币数量并更新储备量。本案例聚焦恒定乘积公式 x * y = k 的核心逻辑,不涉及价格操纵、手续费或治理等扩展功能。

核心实现

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

interface IERC20 {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
}

contract SimpleAMM {
    IERC20 public token0;
    IERC20 public token1;
    uint256 public reserve0;
    uint256 public reserve1;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event AddLiquidity(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpTokens);
    event Sync(uint256 reserve0, uint256 reserve1);

    constructor(address _token0, address _token1) {
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    // 添加流动性:按当前储备比例存入两种代币
    function addLiquidity(uint256 amount0, uint256 amount1) external returns (uint256 lpTokens) {
        require(amount0 > 0 && amount1 > 0, "Amounts must be > 0");

        // 计算应存入的配对数量(按当前储备比例)
        if (reserve0 > 0 || reserve1 > 0) {
            require(reserve0 > 0 && reserve1 > 0, "Invalid reserves");
            uint256 amount1Optimal = (amount0 * reserve1) / reserve0;
            require(amount1 >= amount1Optimal, "Insufficient amount1");
            amount1 = amount1Optimal; // 只接受最优比例,多余退回用户
        }

        // 铸造LP代币数量
        if (totalSupply == 0) {
            lpTokens = sqrt(amount0 * amount1);
        } else {
            lpTokens = min(
                (amount0 * totalSupply) / reserve0,
                (amount1 * totalSupply) / reserve1
            );
        }
        require(lpTokens > 0, "Zero LP tokens");

        // 转账代币到合约
        token0.transferFrom(msg.sender, address(this), amount0);
        token1.transferFrom(msg.sender, address(this), amount1);

        // 更新储备量与LP供应量
        reserve0 += amount0;
        reserve1 += amount1;
        totalSupply += lpTokens;
        balanceOf[msg.sender] += lpTokens;

        emit AddLiquidity(msg.sender, amount0, amount1, lpTokens);
        emit Sync(reserve0, reserve1);
    }

    // 辅助函数
    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }

    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
}

运行与验证

环境依赖:Remix IDE(或Hardhat + ethers.js),需准备两个测试ERC20代币合约(如标准OpenZeppelin ERC20)。

  1. 在Remix中部署两个测试ERC20代币,分别记录地址。
  2. 部署SimpleAMM合约,构造函数传入两个代币地址。
  3. 向AMM合约地址转账测试代币(或直接调用addLiquidity前先给用户授权)。
  4. 调用addLiquidity(amount0, amount1),例如首次添加 amount0=1000, amount1=2000。

验证方法:

  • 调用reserve0()和reserve1(),确认储备量已更新为存入数量。
  • 调用balanceOf(用户地址),确认LP代币数量大于0。
  • 再次调用addLiquidity时传入不成比例的数量(如amount0=500, amount1=2000),合约应自动将amount1调整为按比例的最优值(此处为1000),交易成功且储备量按比例增加。
  • 检查事件AddLiquidity和Sync是否被正确触发。

预期输出(首次添加1000/2000):

  • reserve0 = 1000,reserve1 = 2000
  • totalSupply = sqrt(1000*2000) ≈ 1414(整数截断)
  • 用户LP余额 = 1414

关键说明

  • 首次添加流动性时无比例约束,LP数量取sqrt(amount0 * amount1),防止储备量被稀释。
  • 非首次添加时,合约强制按当前储备比例存入,多余代币不会从用户账户扣除(因transferFrom只转amount1调整后的值)。
  • 本实现未包含滑点保护(minLpTokens参数)和手续费逻辑,实际生产环境需补充。