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合约。

验证流程:

  1. 添加流动性:调用 addLiquidity(1000, 2000)(假设token0=1000, token1=2000),首次添加后 reserve0=1000, reserve1=2000, totalLiquidity=sqrt(1000*2000)=1414。
  2. 交易:调用 swap(token0, 100),预期输出约 amountOut = (100*997*2000)/(1000*1000+100*997) ≈ 181.4。滑点约为 (2.0 - 1.818)/2.0 = 9.1%,即约910 bps。
  3. 移除流动性:调用 removeLiquidity(1414),应返还约 amount0=1000, amount1=2000(扣除交易后的储备变化)。

通过条件:交易后 reserve0 * reserve1 ≥ k(因手续费实际略大于k);移除流动性后用户收到代币数量与LP份额比例一致;滑点随交易规模增大而增大。

滑点计算说明

滑点定义为交易前后价格差异的百分比。合约中 priceBefore 为交易前储备比,priceAfter 为交易后储备比。滑点以bps(基点,1%=100bps)返回,便于链下直接使用。

恒定乘积曲线与滑点示意 token0 储备量 token1 储备量 A (x₀,y₀) B (x₁,y₁) 滑点 = 价格变化率 沿曲线从A移动到B,切线斜率变化即为滑点

常见问题:若交易后 reserve0 * reserve1 < k,说明合约存在溢出或精度错误;添加流动性时若比例不当,合约会按储备比例自动调整实际存入数量,多余部分退回用户(本实现中通过require限制,实际应退回多余代币,简化版直接要求用户按比例输入)。