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)。
部署与调用步骤:
- 部署两个ERC20代币合约,记录地址。
- 部署LiquidityPool,构造函数传入两个代币地址。
- 向池子合约授权代币:
token0.approve(poolAddr, amount)、token1.approve(poolAddr, amount)。 - 调用
mint添加流动性(如各1000枚),获得LP代币。 - 调用
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。此结果由代码逻辑直接唯一推出。
关键说明
本实现采用“先更新状态、后转账”的顺序,可有效防止重入攻击。实际生产环境还需添加滑点保护参数(如最小接收数量)和手续费逻辑,但本案例聚焦于核心份额计算与销毁机制,故不展开。