3. 流动性移除

任务说明

实现流动性移除功能:LP(流动性提供者)调用移除函数后,合约按该LP持有的份额占总供应量的比例,将两种代币(token0/token1)从储备中返还给LP,同时销毁对应数量的LP代币。本案例聚焦于AMM核心逻辑,不涉及价格滑点、手续费等扩展机制。

核心实现

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

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

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

    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1);
    event Sync(uint256 reserve0, uint256 reserve1);

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

    // ---- 仅用于教学演示的辅助函数 ----
    // 实际项目中应由 addLiquidity 调用
    function mint(address to, uint256 amount0, uint256 amount1) external {
        require(IERC20(token0).transferFrom(msg.sender, address(this), amount0), "transfer0 failed");
        require(IERC20(token1).transferFrom(msg.sender, address(this), amount1), "transfer1 failed");
        
        uint256 liquidity;
        if (totalSupply == 0) {
            liquidity = sqrt(amount0 * amount1);
        } else {
            liquidity = min(
                amount0 * totalSupply / reserve0,
                amount1 * totalSupply / reserve1
            );
        }
        require(liquidity > 0, "insufficient liquidity minted");
        
        balanceOf[to] += liquidity;
        totalSupply += liquidity;
        reserve0 += amount0;
        reserve1 += amount1;
        
        emit Mint(to, amount0, amount1);
        emit Sync(reserve0, reserve1);
    }

    // ========== 核心:移除流动性 ==========
    function burn(address to) external returns (uint256 amount0, uint256 amount1) {
        uint256 liquidity = balanceOf[msg.sender];
        require(liquidity > 0, "no LP balance");
        
        // 按LP份额比例计算应返还的代币数量
        amount0 = liquidity * reserve0 / totalSupply;
        amount1 = liquidity * reserve1 / totalSupply;
        require(amount0 > 0 && amount1 > 0, "insufficient liquidity burned");
        
        // 销毁LP代币(先更新状态,防止重入)
        balanceOf[msg.sender] -= liquidity;
        totalSupply -= liquidity;
        
        // 更新储备
        reserve0 -= amount0;
        reserve1 -= amount1;
        
        // 转账代币给LP
        require(IERC20(token0).transfer(to, amount0), "transfer0 failed");
        require(IERC20(token1).transfer(to, amount1), "transfer1 failed");
        
        emit Burn(msg.sender, amount0, amount1);
        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(Solidity 0.8.19+)或Hardhat + ethers.js。需要两个标准ERC20代币合约(如测试用MockToken)。

部署与调用步骤:

  1. 部署两个ERC20代币合约,记录地址。
  2. 部署LiquidityPool,构造函数传入两个代币地址。
  3. 向池子合约授权代币:token0.approve(poolAddr, amount)、token1.approve(poolAddr, amount)。
  4. 调用mint添加流动性(如各1000枚),获得LP代币。
  5. 调用burn移除流动性,参数为接收代币的地址。

验证方法与通过条件:

  • 调用burn后,检查LP的balanceOf变为0,totalSupply归零。
  • 检查LP的token0/token1余额:应分别增加移除前池子储备的对应比例(若LP持有全部份额,则拿回全部储备)。
  • 检查池子的reserve0/reserve1归零。
  • 事件Burn中amount0/amount1与合约实际转出数量一致。

预期输出示例(假设初始mint 1000 token0 + 1000 token1,LP获得1000 LP代币):
调用burn后,LP收到1000 token0和1000 token1,LP余额为0,池子储备为0。此结果由代码逻辑直接唯一推出。

关键说明

本实现采用“先更新状态、后转账”的顺序,可有效防止重入攻击。实际生产环境还需添加滑点保护参数(如最小接收数量)和手续费逻辑,但本案例聚焦于核心份额计算与销毁机制,故不展开。