返回 SC 笔记
SC Day 39

SimpleAMM合约 — 恒定乘积公式与流动性管理

### 一、AMM 核心原理:恒定乘积公式

2026-05-19
第二阶段:框架实战
soliditydefiammuniswap

日期: 2026-05-19 方向: Solidity 阶段: 第二阶段:框架实战 标签: #solidity #defi #amm #uniswap


今日目标

类型内容
学习深入理解 AMM 的恒定乘积公式 x*y=k,掌握添加/移除流动性和 swap 的数学原理
实操实现完整的 SimpleAMM 合约(~150 行),包含 addLiquidity / removeLiquidity / swap
产出完整合约代码 + Foundry 测试 + AMM 数学推导笔记

核心概念

一、AMM 核心原理:恒定乘积公式

AMM(Automated Market Maker,自动做市商)是 DeFi 最重要的创新之一。传统金融用订单簿撮合买卖双方,AMM 则用数学公式和流动性池取代了中间商。

恒定乘积不变量

Uniswap V2 的核心思想极其简洁:

x * y = k

其中:
  x = 池子中 Token A 的数量(reserve0)
  y = 池子中 Token B 的数量(reserve1)
  k = 常数(在不添加/移除流动性的情况下不变)

这个公式定义了一条双曲线。任何 swap 操作都必须沿着这条曲线移动——你放入一种 token,取出另一种 token,但 x * y 的乘积保持不变(扣除手续费后 k 实际上会略微增大)。

为什么是双曲线?

价格曲线图解:

y (Token B)
│
│╲
│ ╲
│  ╲         x * y = k
│   ╲
│    ╲
│     ╲
│      ╲
│       ╲ ╌ ╌ ╌ ╌ ╌
│         ╲
│           ╲
└──────────────────── x (Token A)

关键性质:
  1. 永远不会枯竭: 曲线趋近于轴但永远不接触
  2. 价格连续: 任何交易量都能得到报价
  3. 大额交易 → 沿曲线移动更远 → 价格影响更大

即时价格

在任意时刻,Token A 以 Token B 计价的即时价格为:

Price_A = y / x    (1 个 Token A 值多少 Token B)
Price_B = x / y    (1 个 Token B 值多少 Token A)

例子:
  池子: 100 ETH (x) + 200,000 USDC (y)
  k = 100 * 200,000 = 20,000,000

  ETH 价格 = y / x = 200,000 / 100 = 2,000 USDC/ETH
  USDC 价格 = x / y = 100 / 200,000 = 0.0005 ETH/USDC

二、添加流动性的数学

添加流动性有两种情况:首次添加(池子为空)和后续添加(池子已有储备)。

首次添加流动性

当池子为空时,LP(Liquidity Provider)可以按任意比例添加两种代币,这个比例将决定初始价格。

首次添加:
  LP 存入: dx 个 Token A + dy 个 Token B
  初始价格: Price_A = dy / dx
  初始 k: k = dx * dy

LP Token 计算:
  lpTokens = sqrt(dx * dy)

为什么用 sqrt?
  - LP Token 代表对池子的所有权份额
  - sqrt 确保 LP Token 数量与两种代币数量的"几何平均"成正比
  - 这防止了通过操纵比例来获取不公平份额

数值例子:
  Alice 首次添加: 10 ETH + 20,000 USDC
  k = 10 * 20,000 = 200,000
  lpTokens = sqrt(10 * 20,000) = sqrt(200,000) ≈ 447.21
  初始价格: 20,000 / 10 = 2,000 USDC/ETH

Uniswap V2 的 MINIMUM_LIQUIDITY:实际上 Uniswap V2 在首次铸造时会永久锁定 1000 wei 的 LP Token(发送到 address(0)),防止"首个 LP 攻击"——恶意用户通过极端比例操纵初始流动性来获利。

实际铸造量 = sqrt(dx * dy) - MINIMUM_LIQUIDITY
MINIMUM_LIQUIDITY = 1000 (wei 级别,可忽略不计)

后续添加流动性

池子已有储备时,必须按当前比例添加,否则会改变价格(套利者会立刻抹平差异)。

后续添加:
  池子当前: reserveA = x, reserveB = y, totalSupply = S

  LP 想添加 dx 个 Token A:
    必须同时添加 dy = dx * y / x 个 Token B(按比例)

  LP Token 铸造量 = min(dx / x, dy / y) * S

  使用 min 的原因:
    - 如果 LP 提供的比例不完全匹配,取较小比例
    - 防止通过"多给一种 token"获得不公平的 LP Token
    - 多余的 token 不会铸造额外 LP Token(Uniswap V2 直接退还多余部分)

数值例子:
  池子: 100 ETH + 200,000 USDC, totalSupply = 4472.13 LP
  Bob 想添加 10 ETH:
    需要同时添加: 10 * 200,000 / 100 = 20,000 USDC
    新 LP Token = min(10/100, 20000/200000) * 4472.13
               = min(0.1, 0.1) * 4472.13
               = 447.21 LP

  添加后:
    池子: 110 ETH + 220,000 USDC
    Bob 份额: 447.21 / (4472.13 + 447.21) = 9.09%(合理!)

三、移除流动性

LP 可以通过销毁 LP Token 来按比例取回两种代币。

移除流动性:
  LP 销毁 lpAmount 个 LP Token

  取回 Token A = lpAmount / totalSupply * reserveA
  取回 Token B = lpAmount / totalSupply * reserveB

数值例子:
  池子: 110 ETH + 220,000 USDC, totalSupply = 4919.34 LP
  Bob 持有 447.21 LP,想全部移除:
    取回 ETH = 447.21 / 4919.34 * 110 = 10 ETH
    取回 USDC = 447.21 / 4919.34 * 220,000 = 20,000 USDC

注意: 如果期间有 swap 发生(池子比例变化),Bob 取回的比例会不同于存入时
  → 这就是"无常损失"(Impermanent Loss)的来源

四、Swap 数学:带手续费的精确计算

基本推导

交易前: x * y = k
交易后: (x + dx) * (y - dy) = k    (放入 dx,取出 dy)

由 k 不变:
  x * y = (x + dx) * (y - dy)
  xy = xy - x*dy + dx*y - dx*dy
  0 = -x*dy + dx*y - dx*dy
  x*dy + dx*dy = dx*y
  dy * (x + dx) = dx * y
  dy = dx * y / (x + dx)

结论: amountOut = dx * y / (x + dx)

这就是 getAmountOut 的核心公式!

加入手续费(0.3%)

Uniswap V2 对每笔 swap 收取 0.3% 手续费。手续费从输入金额中扣除,等效于只有 99.7% 的输入金额参与 swap。

加入手续费:
  实际参与 swap 的金额 = dx * 997 / 1000(扣除 0.3%)

  amountOut = (y * dx * 997) / (x * 1000 + dx * 997)

推导过程:
  effectiveDx = dx * 997 / 1000
  amountOut = effectiveDx * y / (x + effectiveDx)
            = (dx * 997 / 1000) * y / (x + dx * 997 / 1000)
            = (dx * 997 * y) / (x * 1000 + dx * 997)

为什么用整数运算而不是浮点数?
  Solidity 没有浮点数!乘以 997/1000 改写为:
    分子: y * dx * 997
    分母: x * 1000 + dx * 997
  全程整数运算,避免精度损失

数值例子:
  池子: 100 ETH (x) + 200,000 USDC (y)
  用户用 1 ETH 换 USDC:
    amountOut = (200,000 * 1 * 997) / (100 * 1000 + 1 * 997)
             = 199,400,000 / (100,000 + 997)
             = 199,400,000 / 100,997
             ≈ 1,974.11 USDC

  对比无手续费:
    amountOut = 1 * 200,000 / (100 + 1) = 1,980.20 USDC

  手续费 ≈ 6.09 USDC(≈ 0.3%)

五、价格影响(Price Impact)

Price Impact 是指一笔交易对池子价格造成的变化百分比。交易量越大(相对于池子深度),价格影响越大。

Price Impact 计算:

交易前价格: P_before = y / x
交易后价格: P_after = (y - amountOut) / (x + amountIn)

Price Impact = (P_before - P_after) / P_before * 100%

例子 1 — 小额交易:
  池子: 100 ETH + 200,000 USDC
  交易: 1 ETH → USDC
  P_before = 200,000 / 100 = 2,000
  amountOut ≈ 1,974.11 USDC(含手续费)
  P_after = (200,000 - 1,974.11) / (100 + 1) = 198,025.89 / 101 ≈ 1,960.65
  Price Impact ≈ (2,000 - 1,960.65) / 2,000 * 100% ≈ 1.97%

例子 2 — 大额交易:
  池子: 100 ETH + 200,000 USDC
  交易: 10 ETH → USDC
  amountOut = (200,000 * 10 * 997) / (100 * 1000 + 10 * 997)
            = 1,994,000,000 / (100,000 + 9,970)
            = 1,994,000,000 / 109,970
            ≈ 18,132.22 USDC
  P_after = (200,000 - 18,132.22) / (100 + 10) = 181,867.78 / 110 ≈ 1,653.34
  Price Impact ≈ (2,000 - 1,653.34) / 2,000 * 100% ≈ 17.33%

结论:
  - 1 ETH swap (1% 的池子): ~2% price impact
  - 10 ETH swap (10% 的池子): ~17% price impact
  - 池子越深 → 同样金额的 price impact 越小 → 更好的交易体验

六、完整 SimpleAMM 合约

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

/**
 * @title SimpleAMM
 * @notice 基于恒定乘积公式(x*y=k)的 AMM 实现
 * @dev 支持添加/移除流动性和 swap,收取 0.3% 手续费
 *      LP Token 继承 ERC20,代表对池子的所有权份额
 */
contract SimpleAMM is ERC20, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // ===== 状态变量 =====
    IERC20 public immutable token0;
    IERC20 public immutable token1;

    uint256 public reserve0;
    uint256 public reserve1;

    uint256 public constant MINIMUM_LIQUIDITY = 1000;
    uint256 private constant FEE_NUMERATOR = 997;
    uint256 private constant FEE_DENOMINATOR = 1000;

    // ===== 自定义错误 =====
    error InvalidToken();
    error InsufficientLiquidity();
    error InsufficientAmount();
    error InsufficientLPTokens();
    error InsufficientOutputAmount();
    error IdenticalTokens();
    error ZeroAmount();
    error KInvariantViolated();

    // ===== 事件 =====
    event LiquidityAdded(
        address indexed provider,
        uint256 amount0,
        uint256 amount1,
        uint256 lpTokensMinted
    );
    event LiquidityRemoved(
        address indexed provider,
        uint256 amount0,
        uint256 amount1,
        uint256 lpTokensBurned
    );
    event Swap(
        address indexed user,
        address indexed tokenIn,
        uint256 amountIn,
        uint256 amountOut
    );

    // ===== 构造函数 =====
    constructor(
        address _token0,
        address _token1
    ) ERC20("SimpleAMM LP Token", "SAMLP") {
        if (_token0 == _token1) revert IdenticalTokens();
        token0 = IERC20(_token0);
        token1 = IERC20(_token1);
    }

    // ===== 核心函数 =====

    /**
     * @notice 添加流动性
     * @param amount0Desired 希望添加的 token0 数量
     * @param amount1Desired 希望添加的 token1 数量
     * @return lpTokens 铸造的 LP Token 数量
     */
    function addLiquidity(
        uint256 amount0Desired,
        uint256 amount1Desired
    ) external nonReentrant returns (uint256 lpTokens) {
        if (amount0Desired == 0 || amount1Desired == 0) revert ZeroAmount();

        uint256 _totalSupply = totalSupply();

        if (_totalSupply == 0) {
            // ===== 首次添加 =====
            // LP Token = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY
            lpTokens = Math.sqrt(amount0Desired * amount1Desired)
                - MINIMUM_LIQUIDITY;
            if (lpTokens == 0) revert InsufficientAmount();

            // 永久锁定 MINIMUM_LIQUIDITY 到 address(1)
            // 防止首个 LP 攻击(totalSupply 永远 > 0)
            _mint(address(1), MINIMUM_LIQUIDITY);
        } else {
            // ===== 后续添加 =====
            // 按比例计算 LP Token
            uint256 lpTokens0 = (amount0Desired * _totalSupply) / reserve0;
            uint256 lpTokens1 = (amount1Desired * _totalSupply) / reserve1;
            lpTokens = lpTokens0 < lpTokens1 ? lpTokens0 : lpTokens1;
            if (lpTokens == 0) revert InsufficientAmount();
        }

        // 转入代币
        token0.safeTransferFrom(msg.sender, address(this), amount0Desired);
        token1.safeTransferFrom(msg.sender, address(this), amount1Desired);

        // 更新储备
        reserve0 += amount0Desired;
        reserve1 += amount1Desired;

        // 铸造 LP Token
        _mint(msg.sender, lpTokens);

        emit LiquidityAdded(
            msg.sender, amount0Desired, amount1Desired, lpTokens
        );
    }

    /**
     * @notice 移除流动性
     * @param lpAmount 要销毁的 LP Token 数量
     * @return amount0 取回的 token0 数量
     * @return amount1 取回的 token1 数量
     */
    function removeLiquidity(
        uint256 lpAmount
    ) external nonReentrant returns (uint256 amount0, uint256 amount1) {
        if (lpAmount == 0) revert ZeroAmount();
        if (balanceOf(msg.sender) < lpAmount) revert InsufficientLPTokens();

        uint256 _totalSupply = totalSupply();

        // 按 LP 份额比例计算取回数量
        amount0 = (lpAmount * reserve0) / _totalSupply;
        amount1 = (lpAmount * reserve1) / _totalSupply;

        if (amount0 == 0 || amount1 == 0) revert InsufficientAmount();

        // 销毁 LP Token
        _burn(msg.sender, lpAmount);

        // 更新储备
        reserve0 -= amount0;
        reserve1 -= amount1;

        // 转出代币
        token0.safeTransfer(msg.sender, amount0);
        token1.safeTransfer(msg.sender, amount1);

        emit LiquidityRemoved(msg.sender, amount0, amount1, lpAmount);
    }

    /**
     * @notice 用 tokenIn 换取另一种 token(收取 0.3% 手续费)
     * @param tokenIn 输入的 token 地址
     * @param amountIn 输入数量
     * @param minAmountOut 最小输出数量(滑点保护)
     * @return amountOut 实际输出数量
     */
    function swap(
        address tokenIn,
        uint256 amountIn,
        uint256 minAmountOut
    ) external nonReentrant returns (uint256 amountOut) {
        if (amountIn == 0) revert ZeroAmount();

        bool isToken0 = (tokenIn == address(token0));
        if (!isToken0 && tokenIn != address(token1)) revert InvalidToken();

        // 确定输入/输出储备
        (uint256 reserveIn, uint256 reserveOut) = isToken0
            ? (reserve0, reserve1)
            : (reserve1, reserve0);

        if (reserveIn == 0 || reserveOut == 0)
            revert InsufficientLiquidity();

        // 计算输出金额(含 0.3% 手续费)
        // amountOut = (reserveOut * amountIn * 997)
        //           / (reserveIn * 1000 + amountIn * 997)
        uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
        amountOut = (reserveOut * amountInWithFee)
            / (reserveIn * FEE_DENOMINATOR + amountInWithFee);

        if (amountOut == 0) revert InsufficientOutputAmount();
        if (amountOut < minAmountOut) revert InsufficientOutputAmount();

        // 转入 tokenIn
        IERC20(tokenIn).safeTransferFrom(
            msg.sender, address(this), amountIn
        );

        // 转出另一种 token 并更新储备
        if (isToken0) {
            reserve0 += amountIn;
            reserve1 -= amountOut;
            token1.safeTransfer(msg.sender, amountOut);
        } else {
            reserve1 += amountIn;
            reserve0 -= amountOut;
            token0.safeTransfer(msg.sender, amountOut);
        }

        // 验证 k 不变量(手续费使 k 只增不减)
        if (reserve0 * reserve1 < (reserveIn * reserveOut)) {
            revert KInvariantViolated();
        }

        emit Swap(msg.sender, tokenIn, amountIn, amountOut);
    }

    // ===== 视图函数 =====

    /**
     * @notice 计算给定输入量的预期输出量
     */
    function getAmountOut(
        address tokenIn,
        uint256 amountIn
    ) external view returns (uint256 amountOut) {
        bool isToken0 = (tokenIn == address(token0));
        if (!isToken0 && tokenIn != address(token1)) revert InvalidToken();

        (uint256 reserveIn, uint256 reserveOut) = isToken0
            ? (reserve0, reserve1)
            : (reserve1, reserve0);

        if (reserveIn == 0 || reserveOut == 0) return 0;

        uint256 amountInWithFee = amountIn * FEE_NUMERATOR;
        amountOut = (reserveOut * amountInWithFee)
            / (reserveIn * FEE_DENOMINATOR + amountInWithFee);
    }

    /**
     * @notice 获取当前价格(token0 以 token1 计价,乘以 1e18 精度)
     */
    function getPrice() external view returns (uint256) {
        if (reserve0 == 0) return 0;
        return (reserve1 * 1e18) / reserve0;
    }

    /**
     * @notice 获取当前 k 值
     */
    function getK() external view returns (uint256) {
        return reserve0 * reserve1;
    }
}

七、Foundry 测试

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

import "forge-std/Test.sol";
import "../src/SimpleAMM.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// ===== Mock Tokens =====
contract MockTokenA is ERC20 {
    constructor() ERC20("Token A", "TKA") {
        _mint(msg.sender, 1_000_000e18);
    }
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

contract MockTokenB is ERC20 {
    constructor() ERC20("Token B", "TKB") {
        _mint(msg.sender, 1_000_000e18);
    }
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

contract SimpleAMMTest is Test {
    SimpleAMM public amm;
    MockTokenA public tokenA;
    MockTokenB public tokenB;

    address public alice = makeAddr("alice");
    address public bob = makeAddr("bob");
    address public charlie = makeAddr("charlie");

    function setUp() public {
        tokenA = new MockTokenA();
        tokenB = new MockTokenB();
        amm = new SimpleAMM(address(tokenA), address(tokenB));

        // 给 alice、bob、charlie 分配代币
        tokenA.transfer(alice, 100_000e18);
        tokenB.transfer(alice, 200_000e18);
        tokenA.transfer(bob, 50_000e18);
        tokenB.transfer(bob, 100_000e18);
        tokenA.transfer(charlie, 10_000e18);
        tokenB.transfer(charlie, 10_000e18);
    }

    // ===== 添加流动性测试 =====

    function test_AddLiquidity_FirstTime() public {
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);

        uint256 lpTokens = amm.addLiquidity(100e18, 200e18);
        vm.stopPrank();

        // LP Token = sqrt(100e18 * 200e18) - 1000
        uint256 expectedLP = Math.sqrt(100e18 * 200e18) - 1000;
        assertEq(lpTokens, expectedLP);
        assertEq(amm.reserve0(), 100e18);
        assertEq(amm.reserve1(), 200e18);
        assertGt(amm.totalSupply(), 0);

        // 验证 MINIMUM_LIQUIDITY 被锁定
        assertEq(amm.balanceOf(address(1)), 1000);
    }

    function test_AddLiquidity_Subsequent() public {
        // Alice 首次添加
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);
        amm.addLiquidity(100e18, 200e18);
        vm.stopPrank();

        uint256 totalSupplyBefore = amm.totalSupply();

        // Bob 后续添加(按比例)
        vm.startPrank(bob);
        tokenA.approve(address(amm), 10e18);
        tokenB.approve(address(amm), 20e18);
        uint256 bobLP = amm.addLiquidity(10e18, 20e18);
        vm.stopPrank();

        // Bob 应该获得 10% 的额外 LP Token
        uint256 expectedBobLP = (10e18 * totalSupplyBefore) / 100e18;
        assertEq(bobLP, expectedBobLP);
        assertEq(amm.reserve0(), 110e18);
        assertEq(amm.reserve1(), 220e18);
    }

    // ===== 移除流动性测试 =====

    function test_RemoveLiquidity() public {
        // Alice 添加流动性
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);
        uint256 aliceLP = amm.addLiquidity(100e18, 200e18);

        // Alice 移除全部流动性
        uint256 balanceA_before = tokenA.balanceOf(alice);
        uint256 balanceB_before = tokenB.balanceOf(alice);

        (uint256 amount0, uint256 amount1) = amm.removeLiquidity(aliceLP);
        vm.stopPrank();

        // 因为 MINIMUM_LIQUIDITY 被锁定,Alice 取不回全部
        assertLt(amount0, 100e18);
        assertLt(amount1, 200e18);
        assertGt(amount0, 99e18);  // 但差距极小
        assertGt(amount1, 199e18);

        // 余额增加
        assertEq(tokenA.balanceOf(alice), balanceA_before + amount0);
        assertEq(tokenB.balanceOf(alice), balanceB_before + amount1);
    }

    // ===== Swap 测试 =====

    function test_Swap_Token0ForToken1() public {
        // Alice 添加流动性
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);
        amm.addLiquidity(100e18, 200e18);
        vm.stopPrank();

        // Bob 用 1 TokenA 换 TokenB
        vm.startPrank(bob);
        tokenA.approve(address(amm), 1e18);

        uint256 expectedOut = amm.getAmountOut(address(tokenA), 1e18);
        uint256 balanceB_before = tokenB.balanceOf(bob);

        uint256 amountOut = amm.swap(address(tokenA), 1e18, 0);
        vm.stopPrank();

        assertEq(amountOut, expectedOut);
        assertEq(tokenB.balanceOf(bob), balanceB_before + amountOut);

        // 验证 k 只增不减
        assertGe(amm.reserve0() * amm.reserve1(), 100e18 * 200e18);
    }

    function test_Swap_SlippageProtection() public {
        // Alice 添加流动性
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);
        amm.addLiquidity(100e18, 200e18);
        vm.stopPrank();

        // Bob 设置过高的 minAmountOut → revert
        vm.startPrank(bob);
        tokenA.approve(address(amm), 1e18);

        vm.expectRevert(SimpleAMM.InsufficientOutputAmount.selector);
        amm.swap(address(tokenA), 1e18, 100e18);
        vm.stopPrank();
    }

    function test_Swap_PriceImpact() public {
        // Alice 添加流动性
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);
        amm.addLiquidity(100e18, 200e18);
        vm.stopPrank();

        // 小额 swap 的单价 vs 大额 swap 的单价
        uint256 smallSwapOut = amm.getAmountOut(address(tokenA), 1e18);
        uint256 largeSwapOut = amm.getAmountOut(address(tokenA), 10e18);

        // 大额交易单价更差(价格影响更大)
        uint256 smallPricePerUnit = (smallSwapOut * 1e18) / 1e18;
        uint256 largePricePerUnit = (largeSwapOut * 1e18) / 10e18;

        assertGt(smallPricePerUnit, largePricePerUnit);
    }

    function test_Swap_FeeAccrual() public {
        // Alice 添加流动性
        vm.startPrank(alice);
        tokenA.approve(address(amm), 100e18);
        tokenB.approve(address(amm), 200e18);
        amm.addLiquidity(100e18, 200e18);
        vm.stopPrank();

        uint256 k_before = amm.getK();

        // Charlie 做 swap(支付手续费)
        vm.startPrank(charlie);
        tokenA.approve(address(amm), 5e18);
        amm.swap(address(tokenA), 5e18, 0);
        vm.stopPrank();

        uint256 k_after = amm.getK();

        // k 应该增加(手续费留在池子中)
        assertGt(k_after, k_before);
    }

    // ===== 边界条件测试 =====

    function test_RevertWhen_SwapWithZeroAmount() public {
        vm.expectRevert(SimpleAMM.ZeroAmount.selector);
        amm.swap(address(tokenA), 0, 0);
    }

    function test_RevertWhen_SwapWithInvalidToken() public {
        vm.expectRevert(SimpleAMM.InvalidToken.selector);
        amm.swap(address(0xdead), 1e18, 0);
    }

    function test_RevertWhen_SwapWithNoLiquidity() public {
        vm.startPrank(bob);
        tokenA.approve(address(amm), 1e18);
        vm.expectRevert(SimpleAMM.InsufficientLiquidity.selector);
        amm.swap(address(tokenA), 1e18, 0);
        vm.stopPrank();
    }
}

关键要点总结

AMM 设计的核心权衡

1. 简单性 vs 资本效率
   恒定乘积 x*y=k 极简优雅,但资本效率低
   → 大部分流动性分布在不常交易的价格区间
   → Uniswap V3 的集中流动性解决了这个问题

2. 价格影响 vs 流动性深度
   池子越深 → 同样金额的 price impact 越小
   → 吸引流动性是 DEX 的核心增长策略
   → 流动性挖矿、交易费分成都是手段

3. 手续费 vs 交易量
   手续费太高 → 交易者被吓走
   手续费太低 → LP 没有激励
   → 0.3% 是经过市场验证的平衡点
   → V3 引入多个费率档位 (0.01%, 0.05%, 0.3%, 1%)

4. LP 收益 vs 无常损失
   LP 赚取手续费,但承受无常损失风险
   → 只有手续费收益 > 无常损失时,做 LP 才划算
   → 高波动性资产对的无常损失更大

SimpleAMM 合约检查清单

安全性:
  ✓ ReentrancyGuard 防止重入攻击
  ✓ SafeERC20 处理非标准 ERC20
  ✓ MINIMUM_LIQUIDITY 防止首个 LP 攻击
  ✓ minAmountOut 滑点保护
  ✓ k 不变量验证(交易后 k 只增不减)

数学精度:
  ✓ 全程整数运算(乘法先于除法,减少精度损失)
  ✓ 手续费用 997/1000 整数比例实现
  ✓ LP Token 用 sqrt 计算(OpenZeppelin Math 库)

生产差距(我们简化了什么):
  ✗ 没有 Oracle 价格(Uniswap V2 有 TWAP)
  ✗ 没有工厂合约(Uniswap 用 Factory 创建交易对)
  ✗ 没有 Router 合约(多跳路由、deadline 等)
  ✗ 没有协议费(Uniswap 可以开启 1/6 手续费给协议)
  ✗ 添加流动性没有退还多余代币

常见误区

误区 1: "x*y=k 中的 k 永远不变"

✗ 错误: k 在任何情况下都是常数
✓ 正确:
  - 纯 swap 时: k 因手续费略微增加(0.3% 留在池子中)
  - 添加流动性时: k 增大(池子变深)
  - 移除流动性时: k 减小(池子变浅)
  - 准确说法: "在单次 swap 中,k 只增不减"

误区 2: "LP Token = 我存入代币的凭证"

✗ 错误: LP Token 只是存款凭证,取回时得到原始存入量
✓ 正确:
  LP Token 代表对池子的按比例所有权
  → 池子比例可能变化(因为有 swap)
  → 取回的代币比例可能不同于存入时
  → 这就是无常损失的根本原因

例子:
  存入: 10 ETH + 20,000 USDC(ETH 价格 2,000)
  ETH 涨到 4,000 后取回:
    可能得到: ~7.07 ETH + 28,284 USDC(总价值低于 HODL)

误区 3: "手续费 0.3% 很低,对交易者影响不大"

✗ 错误: 0.3% 手续费可以忽略
✓ 正确:
  - 多跳路由时手续费累加: ETH→USDC→DAI = 0.6%
  - 大额交易还有 price impact(可能远超手续费)
  - MEV(三明治攻击)可能额外增加成本
  - 对高频交易者,手续费是重要的成本因素

  这也是为什么:
  - 聚合器(1inch/Paraswap)寻找最优路径
  - Uniswap V3 引入 0.01% 费率档位(给稳定币对)
  - CoW Protocol/UniswapX 用 Solver 优化执行

误区 4: "首次添加流动性的 LP 没有风险"

✗ 错误: 首个 LP 最安全
✓ 正确:
  首次 LP 有特殊风险:
  1. 定价风险: 初始比例决定价格,定价偏离市场 → 套利者立刻搬走利润
  2. MINIMUM_LIQUIDITY: 被永久锁定 1000 wei(极小,可忽略)
  3. 首个 LP 攻击(如果没有 MINIMUM_LIQUIDITY):
     - 攻击者添加极少流动性(如 1 wei + 1 wei)
     - 然后直接向池子转账大量代币(不通过 addLiquidity)
     - totalSupply 极低但实际资产极高
     - 后续 LP 因整数截断得到 0 LP Token
     - 攻击者独吞所有新添加的流动性

面试关联

Q1: 解释恒定乘积做市商的工作原理

简短回答:恒定乘积 AMM 用公式 x*y=k 维护两种代币的价格曲线。池子中两种代币的数量乘积保持不变,任何 swap 都沿双曲线滑动——放入一种代币,取出另一种,大额交易造成更大的价格影响。LP 提供流动性赚取 0.3% 手续费,但承受无常损失风险。

追问: 为什么 Uniswap V3 要引入集中流动性?

V2 的问题:
  流动性均匀分布在 0 到 ∞ 的价格范围
  但 99% 的交易发生在很窄的价格区间
  → 大量资本处于"休眠"状态,资本效率极低

V3 的解决方案:
  LP 可以选择在特定价格区间提供流动性
  → 在活跃区间内,相同资本深度更大
  → 资本效率提升 4000 倍(理论上)

代价:
  → LP 需要主动管理头寸(价格脱离区间后不赚手续费)
  → 无常损失可能更大(集中 = 放大风险)
  → JIT (Just-In-Time) Liquidity 攻击

Q2: 如何防止 AMM 中的价格操纵?

AMM 价格操纵的原理:
  攻击者用大额 swap 扭曲池子价格
  → 其他协议如果依赖即时价格(spot price)作为预言机
  → 攻击者可以在同一笔交易中利用扭曲的价格获利

防御方法:
  1. TWAP (Time-Weighted Average Price)
     → 使用跨区块的时间加权平均价,无法在单笔交易中操纵
  2. Chainlink 预言机
     → 使用链下聚合的多数据源价格
  3. 流动性深度要求
     → 池子足够深时,操纵成本极高
  4. 延迟机制
     → commit-reveal 防止同笔交易中的价格依赖

Q3: 什么是无常损失?LP 什么时候做 LP 是划算的?

无常损失公式:
  IL = 2 * sqrt(priceRatio) / (1 + priceRatio) - 1

  价格变化 2x → IL ≈ -5.7%
  价格变化 5x → IL ≈ -25.5%

LP 做市划算的条件:
  手续费收入 > 无常损失

  有利场景:
  ✓ 高交易量的交易对(手续费多)
  ✓ 低波动性资产对(如 USDC/USDT,几乎无 IL)
  ✓ 双向交易均衡(价格回归)

  不利场景:
  ✗ 低交易量 + 高波动(手续费少、IL 大)
  ✗ 单向价格移动(如 meme coin 暴涨然后归零)

参考资源

资源说明
Uniswap V2 白皮书恒定乘积 AMM 的原始设计,10 页必读
Uniswap V2 Core 源码生产级 AMM 实现,对照学习
Uniswap V3 白皮书集中流动性的数学推导
Pintail: Uniswap - A Good Deal for LPs?无常损失的经典分析
Solidity by Example - AMM简化版教学代码
OpenZeppelin Math.solsqrt 实现参考