教程区块链区块链基础知识chunk_55_ch19_dex_pt1第19章 恒定乘积公式与流动性池合约

本页目录

前置依赖:掌握 ERC-20 标准(第18章)与 Solidity 合约开发(第17章)后,本章将带你深入 DeFi(去中心化金融,Decentralized Finance)的基石——自动化做市商(AMM, Automated Market Maker)。我们将从数学公式出发,逐步推导并手写一个与 Uniswap V2 核心逻辑等价的最简流动性池合约。

19.1 恒定乘积公式与核心算法

19.1.1 从订单簿到自动化做市商

传统中心化交易所(如股票交易所或中心化加密交易所)依赖订单簿(Order Book)撮合交易:买方挂出「愿意以某价格买入」的限价单,卖方挂出「愿意以某价格卖出」的限价单,系统寻找双方价格重叠的订单进行撮合。这种模式的痛点在于:若某一价格区间缺乏挂单,则大额交易会面临巨大滑点(Slippage);同时,专业做市商需要持续在订单簿两侧挂单,准入门槛极高。

AMM 的核心思想是用一段数学公式替代人工订单簿。流动性提供者(Liquidity Provider, LP)将一对代币存入一个流动性池(Liquidity Pool),池中的两种代币数量始终遵循一条预设的定价曲线(Pricing Curve)。任何交易者无需等待对手方出现,只需将代币 A 投入池中,即可按曲线规定的价格自动换回代币 B——合约本身就成了「永不休息的全自动做市商」。

Uniswap 于 2018 年首次在以太坊主网上大规模实现了恒定乘积做市商(Constant Product Market Maker, CPMM),成为 DeFi Summer 的基础设施层。本章将以 Uniswap V2 为蓝本,从公式推导、合约实现到经济分析,完成一个最小可用的「Pair 合约」。

19.1.2 恒定乘积公式的数学推导

设池中有代币 \( X \) 的数量为 \( x \),代币 \( Y \) 的数量为 \( y \)。CPMM 的核心约束是一条极其简洁的方程:

\[

x \times y = k

\]

其中 \( k \) 为恒定乘积常数。在无手续费且无流动性增减的情况下,\( k \) 保持不变。

交换推导:假设某用户向池中投入 \( \Delta x \) 个代币 \( X \),则池中 \( X \) 的储备变为 \( x + \Delta x \)。为维持乘积 \( k \) 恒定,池中剩余代币 \( Y \) 的数量必须调整为 \( y' \):

\[

(x + \Delta x) \times y' = k \quad \Rightarrow \quad y' = \frac{k}{x + \Delta x}

\]

用户能够获得的代币 \( Y \) 数量即为两者的差值:

\[

\Delta y = y - y' = y - \frac{x \times y}{x + \Delta x} = \frac{y \times \Delta x}{x + \Delta x}

\]

这个公式揭示了一个关键直觉:池中两种代币的储备比例越不平衡,同样的输入 \( \Delta x \) 能换回的输出 \( \Delta y \) 就越少。这正是滑点的数学根源。同时,一个美妙的边界条件是:当 \( \Delta x \to \infty \) 时,\( \Delta y \to y \)——这意味着理论上你永远无法把池中一种代币全部换走,流动性永不枯竭

19.1.3 AMM 价格曲线与即时价格

对恒定乘积方程 \( x \times y = k \) 两边求导,可得曲线在点 \( (x, y) \) 处的切线斜率。我们关心的边际价格(Instantaneous Price),即当前时刻 1 个代币 \( X \) 以代币 \( Y \) 计价的价格,为:

\[

P = \frac{y}{x}

\]

当一笔交易买入代币 \( X \)(即向池中增加 \( Y \) 并取出 \( X \))后,\( x \) 减小而 \( y \) 增大,新价格 \( P' = y' / x' \) 随之上升——合约自动「涨价」抑制进一步购买。反之亦然。价格发现被编码在储备量的变化之中,无需外部喂价。

CPMM 的价格曲线在坐标系中是一条双曲线 \( y = k / x \)。与订单簿的阶梯式深度图不同,AMM 的深度由总流动性(即 \( k \) 的大小)直接决定:\( k \) 越大,同样的交易量对价格的影响越小,滑点越低。

19.1.4 LP Token 经济学与无常损失

流动性提供者将代币对存入池中后,合约会铸造一种池份额代币(LP Token)作为凭证。LP Token 本身通常也是标准 ERC-20 代币,代表其对池中两种资产的所有权比例。

首次添加流动性时,铸造的 LP Token 总量为:

\[

\text{minted} = \sqrt{x \times y} - \text{MINIMUM\_LIQUIDITY}

\]

其中 MINIMUM_LIQUIDITY = 1000(由 Uniswap V2 定义),这部分最小份额被永久锁定至地址 0 以消除除零风险。后续添加流动性时,则按存入比例铸造:

\[

\text{minted} = \frac{\Delta x}{x} \times \text{totalSupply}

\]

(实际实现中取按代币 A 和代币 B 计算出的较小值,多余代币会原路退回。)

当外部市场价格相对存入时刻发生偏离时,套利者会不断在池中交易,使合约内部价格向外部市场价格靠拢。这导致 LP 持有的资产比例被强制再平衡。数学上,若存入后价格变化比率为 \( r \),LP 相对于「简单持有(HODL)」的价值损失比例可近似为:

\[

\text{IL} = \frac{2\sqrt{r}}{1+r} - 1

\]

这就是著名的无常损失(Impermanent Loss, IL)。好消息是,每笔交易 0.3% 的手续费不会直接分配给 LP,而是直接累积到池中——表现为 \( k \) 值随时间缓慢增长。只要手续费收益超过无常损失,LP 的净收益仍可为正。手续费收益是 LP 承担价格风险的补偿机制。

本节要点小结

  • \( x \times y = k \) 是 CPMM 的唯一核心约束,所有价格与滑点均可由该公式推演。
  • 边际价格 \( P = y / x \) 让合约在无外部喂价的情况下自动完成价格发现。
  • LP 面临无常损失,但手续费累积的 \( k \) 增长是其风险补偿来源。

19.2 流动性池合约

19.2.1 添加流动性:接口与决策流程

在 Uniswap V2 风格的设计中,流动性由 Router 合约代理用户与底层 Pair 合约交互。addLiquidity 的函数签名如下:

solidity
function addLiquidity(
    address tokenA,
    address tokenB,
    uint256 amountADesired,
    uint256 amountBDesired,
    uint256 amountAMin,
    uint256 amountBMin,
    address to,
    uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);

其核心决策流程可用 Mermaid 流程图表示:

flowchart TD
    A[用户调用 addLiquidity] --> B{池是否已存在?}
    B -->|reserve0 == 0 <br/> 首次添加| C[LP Token = sqrt(amountA * amountB) - 1000 <br/> 设定初始比率]
    B -->|reserve0 > 0 <br/> 已存在池| D[按当前储备计算 optimalB = <br/> amountADesired * reserve1 / reserve0]
    D --> E{optimalB <= amountBDesired?}
    E -->|是| F[存入 amountA = amountADesired <br/> amountB = optimalB]
    E -->|否| G[存入 amountB = amountBDesired <br/> 反向计算 optimalA]
    C --> H[amountA >= amountAMin? <br/> amountB >= amountBMin?]
    F --> H
    G --> H
    H -->|否| I[revert: 滑点超限]
    H -->|是| J[从用户转入代币到 Pair]
    J --> K[铸造 LP Token 给地址 to]
    K --> L[_update 更新储备量 <br/> 触发 Sync 事件]

关键安全设计

  1. 地址排序:调用前必须校验 tokenA < tokenB,保证同一交易对不会因参数顺序不同而生成重复池子。
  2. 最小值参数(amountAMin / amountBMin):防止 MEV 抢跑者(Front-runner)在用户交易Pending期间操控池价,导致用户以劣于预期的比率存入。若实际比率低于用户容忍下限,交易回滚。
  3. 截止时间(deadline):避免交易在内存池中挂起过久,在极端行情下被矿工在数小时后执行。

19.2.2 移除流动性:赎回与销毁

solidity
function removeLiquidity(
    address tokenA,
    address tokenB,
    uint256 liquidity,
    uint256 amountAMin,
    uint256 amountBMin,
    address to,
    uint256 deadline
) external returns (uint256 amountA, uint256 amountB);

移除流动性的核心逻辑是按 LP Token 份额比例赎回两种储备资产:

\[

\text{amountA} = \frac{\text{liquidity}}{\text{totalSupply}} \times \text{reserve0}, \quad

\text{amountB} = \frac{\text{liquidity}}{\text{totalSupply}} \times \text{reserve1}

\]

合约先销毁用户的 LP Token(内部状态变更),再通过 safeTransfer 将对应代币转出——严格遵循「检查-生效-交互」(Checks-Effects-Interactions)模式,避免重入攻击。实际赎回数量同样不得低于 amountAMinamountBMin,否则回滚。移除后需调用 _update() 同步储备量并触发 Sync 事件,供链下索引器重建状态。

19.2.3 完整 Pair 合约架构:最小化实现

以下是仿制 Uniswap V2 的最简 Pair 合约骨架,包含 mint()burn()、核心数学库函数及关键状态变量:

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

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

contract Pair is ERC20 {
    // ---------- 常量 ----------
    uint256 public constant MINIMUM_LIQUIDITY = 1000;
    // 手续费 0.3% 用 3/1000 表示
    uint256 public constant FEE_DENOMINATOR = 1000;
    uint256 public constant FEE_NUMERATOR  = 3;

    // ---------- 状态变量 ----------
    address public token0;
    address public token1;
    uint256 public reserve0;          // token0 储备量(含缓存)
    uint256 public reserve1;          // token1 储备量(含缓存)
    uint32  public blockTimestampLast; // 上次更新时间戳(用于 TWAP 预言机)
    uint256 public price0CumulativeLast;
    uint256 public price1CumulativeLast;

    // ---------- 事件 ----------
    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
    event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to);
    event Sync(uint256 reserve0, uint256 reserve1);

    constructor(address _token0, address _token1) ERC20("LP-Token", "LP") {
        token0 = _token0;
        token1 = _token1;
    }

    // ---------- 核心:_update 同步储备 ----------
    function _update(uint256 balance0, uint256 balance1) private {
        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed  = blockTimestamp - blockTimestampLast;
        if (timeElapsed > 0 && reserve0 != 0 && reserve1 != 0) {
            // 时间加权累计价格(TWAP 基础)
            price0CumulativeLast += uint256(reserve1) * timeElapsed / reserve0;
            price1CumulativeLast += uint256(reserve0) * timeElapsed / reserve1;
        }
        reserve0 = balance0;
        reserve1 = balance1;
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }

    // ---------- 添加流动性:mint ----------
    function mint(address to) external returns (uint256 liquidity) {
        (uint256 _reserve0, uint256 _reserve1) = (reserve0, reserve1);
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 amount0  = balance0 - _reserve0;
        uint256 amount1  = balance1 - _reserve1;

        uint256 _totalSupply = totalSupply();
        if (_totalSupply == 0) {
            // 首次添加流动性
            liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
            _mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定
        } else {
            // 按比例铸造,取较小值保证不被套利
            liquidity = Math.min(
                (amount0 * _totalSupply) / _reserve0,
                (amount1 * _totalSupply) / _reserve1
            );
        }
        require(liquidity > 0, "INSUFFICIENT_LIQUIDITY_MINTED");
        _mint(to, liquidity);
        _update(balance0, balance1);
        emit Mint(msg.sender, amount0, amount1);
    }

    // ---------- 移除流动性:burn ----------
    function burn(address to) external returns (uint256 amount0, uint256 amount1) {
        uint256 _totalSupply = totalSupply();
        uint256 liquidity    = balanceOf(address(this)); // 用户先转入 LP Token
        require(liquidity > 0 && _totalSupply > 0, "NO_LIQUIDITY");

        amount0 = (liquidity * reserve0) / _totalSupply;
        amount1 = (liquidity * reserve1) / _totalSupply;
        require(amount0 > 0 && amount1 > 0, "INSUFFICIENT_LIQUIDITY_BURNED");

        _burn(address(this), liquidity);
        _safeTransfer(token0, to, amount0);
        _safeTransfer(token1, to, amount1);

        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        _update(balance0, balance1);
        emit Burn(msg.sender, amount0, amount1, to);
    }

    // ---------- swap 骨架(含手续费) ----------
    function swap(uint256 amount0Out, uint256 amount1Out, address to) external {
        require(amount0Out > 0 || amount1Out > 0, "INSUFFICIENT_OUTPUT");

        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 _reserve0 = reserve0;
        uint256 _reserve1 = reserve1;

        require(amount0Out < balance0 && amount1Out < balance1, "INSUFFICIENT_LIQUIDITY");

        if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
        if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);

        uint256 balance0Adjusted = balance0 * 1000 - amount0Out * 3;
        uint256 balance1Adjusted = balance1 * 1001 - amount1Out * 3;
        // 延至 19.3 展开完整输入量校验与 k 值检查

        // 占位:实际需校验 (balance0 * balance1) >= (_reserve0 * _reserve1)
        _update(
            IERC20(token0).balanceOf(address(this)),
            IERC20(token1).balanceOf(address(this))
        );
        emit Swap(msg.sender, 0, 0, amount0Out, amount1Out, to);
    }

    // ---------- 库函数:含 0.3% 手续费的输出量计算 ----------
    function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
        public pure returns (uint256 amountOut)
    {
        require(amountIn > 0, "INSUFFICIENT_INPUT");
        require(reserveIn > 0 && reserveOut > 0, "INSUFFICIENT_LIQUIDITY");
        uint256 amountInWithFee = amountIn * 997; // 扣除 0.3% 手续费
        uint256 numerator   = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        amountOut = numerator / denominator;
    }

    // ---------- 安全转账 ----------
    function _safeTransfer(address token, address to, uint256 amount) private {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.transfer.selector, to, amount)
        );
        require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED");
    }
}

代码要点解读

  1. LP Token 即 ERC-20Pair 合约自身继承 ERC20,LP 份额本身是可转移、可交易的代币。
  2. _update() 的关键作用:每次储备量变更后调用,同步缓存的 reserve0/1 并更新 TWAP 累计价格。这些累计值可供外部预言机按时间窗口求平均,获得不易被闪电贷操纵的参考价格。
  3. 含手续费的输出公式:实际链上 getAmountOut 中,\( \Delta x \) 先乘以 997(即扣除 0.3% 手续费后的 99.7%),再代入恒定乘积公式:

\[

\Delta y = \frac{y \times (\Delta x \times 997)}{(x \times 1000) + (\Delta x \times 997)}

\]

  1. 安全防护模式
  • 地址排序防重复池:由上层 Factory 合约在创建 Pair 时强制 token0 < token1
  • 极小值参数(min 参数)防抢跑:Router 合约在调用 Pair 前校验。
  • 检查-生效-交互模式:burn 先销毁 LP Token(状态变更),再转出代币(外部调用)。

本节要点小结

  • addLiquidity 通过 amountAMin / amountBMindeadline 保护用户免受滑点与抢跑。
  • mint()burn() 的份额计算确保 LP 始终按比例持有池中资产,首次流动性扣除 1000 个最小锁定份额。
  • _update() 与 TWAP 累计价格是链上价格预言机的基础,将在 19.3 节 swap 安全中发挥关键作用。

章末小结 —— 带走的 3 个关键认知

  1. 恒定乘积公式 \( x \times y = k \) 是 AMM 的灵魂:它同时决定了价格自动调整、滑点曲线和流动性永不枯竭的数学保障。所有 DEX 经济行为——从单笔 swap 的成交价格到 LP 的无常损失——均可从该公式推演。
  1. LP Token 是将流动性数字化的份额凭证:铸造/销毁机制确保 LP 始终按份额比例持有池中两种资产;交易手续费不直接分配,而是作为储备增量不断推高 \( k \) 值,LP 的份额所对应的绝对资产量随之增长。
  1. 安全的流动性管理需严格遵循 Solidity 最佳实践:地址排序防重复池、最小值参数防抢跑、检查-生效-交互模式防重入——这些看似简单的模式,是 DeFi 合约从数亿级攻击中存活下来的基石。

评论

0

评论加载中…

发表评论

0/2000