1. 恒定乘积做市商基础实现
任务目标
实现一个Uniswap V2风格的恒定乘积做市合约,核心逻辑为 x * y = k。合约需支持:添加流动性、移除流动性、代币兑换(交易),并在交易时计算并返回滑点。本案例聚焦AMM核心数学与合约逻辑,不涉及路由、闪电贷等外围功能。
环境依赖
- Solidity ^0.8.0
- 开发框架:Hardhat 或 Foundry(任选,本案例以纯Solidity合约为主)
- 测试代币:需两个标准ERC20代币(可直接使用OpenZeppelin的ERC20PresetMinterPauser或自写最小ERC20)
核心实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract ConstantProductAMM {
IERC20 public token0;
IERC20 public token1;
uint256 public reserve0;
uint256 public reserve1;
uint256 public totalLiquidity;
mapping(address => uint256) public liquidity;
event AddLiquidity(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpTokens);
event RemoveLiquidity(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpTokens);
event Swap(address indexed trader, address indexed tokenIn, uint256 amountIn, uint256 amountOut, uint256 slippageBps);
constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
// 添加流动性:按当前储备比例存入两种代币
function addLiquidity(uint256 amount0Desired, uint256 amount1Desired) external returns (uint256 lpTokens) {
require(amount0Desired > 0 && amount1Desired > 0, "Amounts must be > 0");
uint256 amount0;
uint256 amount1;
if (totalLiquidity == 0) {
// 首次添加:按给定数量直接存入
amount0 = amount0Desired;
amount1 = amount1Desired;
lpTokens = sqrt(amount0 * amount1);
} else {
// 非首次:按当前储备比例计算实际应存入数量
amount1 = (amount1Desired * reserve0) / reserve1;
require(amount1 <= amount1Desired, "Insufficient amount1");
amount0 = amount0Desired;
lpTokens = (totalLiquidity * amount0) / reserve0;
}
require(lpTokens > 0, "Zero LP tokens");
token0.transferFrom(msg.sender, address(this), amount0);
token1.transferFrom(msg.sender, address(this), amount1);
reserve0 += amount0;
reserve1 += amount1;
liquidity[msg.sender] += lpTokens;
totalLiquidity += lpTokens;
emit AddLiquidity(msg.sender, amount0, amount1, lpTokens);
}
// 移除流动性:销毁LP代币,按比例返还两种代币
function removeLiquidity(uint256 lpTokens) external returns (uint256 amount0, uint256 amount1) {
require(lpTokens > 0 && liquidity[msg.sender] >= lpTokens, "Invalid LP amount");
amount0 = (reserve0 * lpTokens) / totalLiquidity;
amount1 = (reserve1 * lpTokens) / totalLiquidity;
liquidity[msg.sender] -= lpTokens;
totalLiquidity -= lpTokens;
reserve0 -= amount0;
reserve1 -= amount1;
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
emit RemoveLiquidity(msg.sender, amount0, amount1, lpTokens);
}
// 交易:恒定乘积 x*y=k,收取0.3%手续费
function swap(address tokenIn, uint256 amountIn) external returns (uint256 amountOut, uint256 slippageBps) {
require(tokenIn == address(token0) || tokenIn == address(token1), "Invalid ");
require(amountIn > 0, "Amount must be > 0");
bool isToken0In = (tokenIn == address(token0));
uint256 reserveIn = isToken0In ? reserve0 : reserve1;
uint256 reserveOut = isToken0In ? reserve1 : reserve0;
uint256 amountInWithFee = amountIn * 997; // 0.3% 手续费
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
// 滑点 = (实际成交价 - 交易前价格) / 交易前价格,以bps表示
uint256 priceBefore = (reserveOut * 1e18) / reserveIn;
uint256 priceAfter = ((reserveOut - amountOut) * 1e18) / (reserveIn + amountIn);
slippageBps = (priceBefore > priceAfter) ? (priceBefore - priceAfter) * 10000 / priceBefore : 0;
if (isToken0In) {
token0.transferFrom(msg.sender, address(this), amountIn);
token1.transfer(msg.sender, amountOut);
reserve0 += amountIn;
reserve1 -= amountOut;
} else {
token1.transferFrom(msg.sender, address(this), amountIn);
token0.transfer(msg.sender, amountOut);
reserve1 += amountIn;
reserve0 -= amountOut;
}
emit Swap(msg.sender, tokenIn, amountIn, amountOut, slippageBps);
}
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;
}
}
}
部署与验证
前置准备:部署两个ERC20测试代币合约,并将代币地址作为构造参数部署AMM合约。
验证流程:
- 添加流动性:调用
addLiquidity(1000, 2000)(假设token0=1000, token1=2000),首次添加后reserve0=1000,reserve1=2000,totalLiquidity=sqrt(1000*2000)=1414。 - 交易:调用
swap(token0, 100),预期输出约amountOut = (100*997*2000)/(1000*1000+100*997) ≈ 181.4。滑点约为(2.0 - 1.818)/2.0 = 9.1%,即约910 bps。 - 移除流动性:调用
removeLiquidity(1414),应返还约amount0=1000,amount1=2000(扣除交易后的储备变化)。
通过条件:交易后 reserve0 * reserve1 ≥ k(因手续费实际略大于k);移除流动性后用户收到代币数量与LP份额比例一致;滑点随交易规模增大而增大。
滑点计算说明
滑点定义为交易前后价格差异的百分比。合约中 priceBefore 为交易前储备比,priceAfter 为交易后储备比。滑点以bps(基点,1%=100bps)返回,便于链下直接使用。
常见问题:若交易后 reserve0 * reserve1 < k,说明合约存在溢出或精度错误;添加流动性时若比例不当,合约会按储备比例自动调整实际存入数量,多余部分退回用户(本实现中通过require限制,实际应退回多余代币,简化版直接要求用户按比例输入)。