分类: DeFi技术原理

  • AMM自动做市商核心原理深度解析:从数学公式到合约实现

    AMM自动做市商核心原理深度解析:从数学公式到合约实现

    为什么需要AMM

    在说AMM之前,先理解传统做市商的问题。

    在股票、外汇这些传统金融市场,撮合买卖双方的是订单簿模式:买方出价、卖方报价,系统匹配后成交。这个模式需要专业的做市商持续报价、撤单、调价,对流动性要求很高。

    区块链的匿名性和交易延迟让订单簿模式很难直接套用。 于是有人提出了一个天才的想法:与其让人工做市,不如让算法做市

    AMM(Automated Market Maker,自动做市商)的核心思想很简单:用数学公式替代订单簿,买卖价格由算法自动计算。用户不需要等待对手方,只需要和一个”资金池”交易即可。

    无常损失数据表格,展示不同价格波动下的损失比例从2%到42.7%

    恒定乘积做市商( CPMM )的数学原理

    Uniswap采用的核心算法叫做恒定乘积做市商(Constant Product Market Maker),公式非常简洁:

    plaintext

    x * y = k
    

    其中:

    • x = 资金池中Token A的数量
    • y = 资金池中Token B的数量
    • k = 恒定乘积(交易前后不变)

    交易机制推导

    假设交易前资金池有 (x₁, y₁),满足 x₁ * y₁ = k

    用户想用 Δx 个Token A换取Token B。交易后资金池变为 (x₂, y₂),需要满足:

    plaintext

    x₂ = x₁ + Δx  (资金池多了Δx的Token A)
    x₂ * y₂ = k   (恒定乘积不变)
    y₂ = k / x₂
    

    因此用户获得的Token B数量为:

    plaintext

    Δy = y₁ - y₂
        = y₁ - k / (x₁ + Δx)
        = y₁ - (x₁ * y₁) / (x₁ + Δx)
        = y₁ * (1 - x₁ / (x₁ + Δx))
        = y₁ * Δx / (x₁ + Δx)
    

    这就是AMM的定价公式。让我用一个具体例子验证:

    假设资金池初始状态:x₁ = 1000 ETHy₁ = 2000000 USDC,则 k = 2,000,000,000

    用户想用 Δx = 10 ETH 购买USDC:

    plaintext

    Δy = 2000000 * 10 / (1000 + 10) = 20000000 / 1010 ≈ 19801.98 USDC
    

    即用户用10 ETH换到了约19802 USDC。价格约为 1 ETH = 1980.2 USDC,与当前市场价格基本一致。

    大额交易的影响:滑点

    继续上面的例子,如果用户要用 100 ETH 购买USDC呢?

    plaintext

    Δy = 2000000 * 100 / (1000 + 100) = 200000000 / 1100 ≈ 181818.18 USDC
    

    用户得到了181818 USDC,但按市场价应该得到约198000 USDC(假设1 ETH = 1980 USDC)。差了约16000 USDC,这就是滑点。

    滑点随着交易规模增大而急剧增加,这是AMM的核心限制之一。资金池越深(x和y越大),相同交易量的滑点越小。

    无常损失:LP的核心风险

    作为流动性提供者(LP),你需要理解一个关键概念:无常损失(Impermanent Loss)

    什么是无常损失

    假设ETH价格是2000 USDC。你作为LP,向资金池投入了:

    • 1 ETH(价值2000 USDC)
    • 2000 USDC

    资金池总价值:4000 USDC。你占总池子的0.1%(假设总池子价值400万USDC)。

    价格变化后

    假设ETH涨到4000 USDC。在AMM机制下,资金池会自动调整:

    plaintext

    x * y = k
    x * y = 1000 * 2000000 = 2,000,000,000
    
    当 x * 4000 = 2,000,000,000 时:
    x = 500 ETH
    y = 500,000 USDC
    
    池子总价值 = 500 * 4000 + 500,000 = 2,500,000 USDC
    

    如果你没有提供流动性,而是持有原来的1 ETH + 2000 USDC:
    总价值 = 1 * 4000 + 2000 = 6000 USDC

    无常损失 = 6000 – 5500 = 500 USDC(约8.3%)

    数学公式

    无常损失可以用一个简洁的公式表达:

    plaintext

    无常损失 = 2 * sqrt(价格比率) / (1 + 价格比率) - 1
    

    当价格比率变为原来的k倍时:

    价格变化无常损失
    ±50%-2.0%
    ±100%-5.7%
    ±200%-13.4%
    ±400%-25.5%
    ±800%-42.7%

    价格波动越大,无常损失越严重。 这也是为什么Uniswap V3允许LP集中流动性——他们希望用更高的手续费收益来对冲无常损失。

    简化版AMM合约实现

    现在来动手实现一个简化版的Uniswap AMM合约:

    solidity

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    
    /**
     * @title SimpleAMM
     * @notice 简化版恒定乘积做市商合约
     */
    contract SimpleAMM {
        // 代币A的储备量
        uint256 public reserveA;
        // 代币B的储备量
        uint256 public reserveB;
        
        // 两种代币的接口
        IERC20 public tokenA;
        IERC20 public tokenB;
        
        // 手续费比例(基点,100 = 1%)
        uint256 public fee = 30;  // 0.3% 手续费
        
        event Swap(
            address indexed user,
            address indexed tokenIn,
            address indexed tokenOut,
            uint256 amountIn,
            uint256 amountOut
        );
        event AddLiquidity(
            address indexed provider,
            uint256 amountA,
            uint256 amountB
        );
        event RemoveLiquidity(
            address indexed provider,
            uint256 amountA,
            uint256 amountB
        );
        
        constructor(address _tokenA, address _tokenB) {
            tokenA = IERC20(_tokenA);
            tokenB = IERC20(_tokenB);
        }
        
        /**
         * @notice 添加流动性(首次添加时初始化资金池)
         */
        function addLiquidity(uint256 amountA, uint256 amountB) external returns (uint256 liquidity) {
            require(amountA > 0 && amountB > 0, "Invalid amounts");
            
            // 首次添加:初始化储备量
            if (reserveA == 0 && reserveB == 0) {
                tokenA.transferFrom(msg.sender, address(this), amountA);
                tokenB.transferFrom(msg.sender, address(this), amountB);
                
                reserveA = amountA;
                reserveB = amountB;
                liquidity = amountA;  // 首次添加的liquidity代币数量
            } else {
                // 检查比例是否正确
                require(
                    amountA * reserveB == amountB * reserveA,
                    "Invalid ratio"
                );
                
                tokenA.transferFrom(msg.sender, address(this), amountA);
                tokenB.transferFrom(msg.sender, address(this), amountB);
                
                // 按比例计算新增的liquidity
                uint256 totalLiquidity = reserveA;  // 简化:用reserveA代表总liquidity
                liquidity = (amountA * totalLiquidity) / reserveA;
                
                reserveA += amountA;
                reserveB += amountB;
            }
            
            emit AddLiquidity(msg.sender, amountA, amountB);
        }
        
        /**
         * @notice 移除流动性
         */
        function removeLiquidity(uint256 liquidity) external returns (uint256 amountA, uint256 amountB) {
            require(liquidity > 0, "Invalid liquidity");
            
            uint256 totalLiquidity = reserveA;  // 简化
            
            amountA = (liquidity * reserveA) / totalLiquidity;
            amountB = (liquidity * reserveB) / totalLiquidity;
            
            reserveA -= amountA;
            reserveB -= amountB;
            
            tokenA.transfer(msg.sender, amountA);
            tokenB.transfer(msg.sender, amountB);
            
            emit RemoveLiquidity(msg.sender, amountA, amountB);
        }
        
        /**
         * @notice 交换代币
         * @param tokenIn 输入代币地址
         * @param amountIn 输入数量
         * @param amountOutMin 最小输出数量(防止大滑点)
         */
        function swap(
            address tokenIn,
            uint256 amountIn,
            uint256 amountOutMin
        ) external returns (uint256 amountOut) {
            require(tokenIn == address(tokenA) || tokenIn == address(tokenB), "Invalid token");
            require(amountIn > 0, "Invalid amount");
            
            (uint256 reserveIn, uint256 reserveOut, address tokenOut) = 
                tokenIn == address(tokenA) 
                    ? (reserveA, reserveB, address(tokenB))
                    : (reserveB, reserveA, address(tokenA));
            
            // 计算手续费(扣除0.3%)
            uint256 amountInWithFee = amountIn * (1000 - fee);
            
            // 恒定乘积公式计算输出
            // Δy = (Δx * y) / (x + Δx)
            amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
            
            require(amountOut >= amountOutMin, "Slippage exceeded");
            require(amountOut < reserveOut, "Insufficient reserve");
            
            // 执行转账
            IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
            IERC20(tokenOut).transfer(msg.sender, amountOut);
            
            // 更新储备量
            if (tokenIn == address(tokenA)) {
                reserveA += amountIn;
                reserveB -= amountOut;
            } else {
                reserveB += amountIn;
                reserveA -= amountOut;
            }
            
            emit Swap(msg.sender, tokenIn, tokenOut, amountIn, amountOut);
        }
        
        /**
         * @notice 获取当前价格(1个A能换多少B)
         */
        function getPrice(address tokenIn) public view returns (uint256) {
            require(tokenIn == address(tokenA) || tokenIn == address(tokenB), "Invalid token");
            
            uint256 reserveIn = tokenIn == address(tokenA) ? reserveA : reserveB;
            uint256 reserveOut = tokenIn == address(tokenA) ? reserveB : reserveA;
            
            // 边际价格:dy/dx = y/x
            return (reserveOut * 1e18) / reserveIn;
        }
    }
    

    实际部署需要注意的问题

    上面的简化版合约存在几个问题,生产环境需要额外处理:

    1. 重入攻击防护

    AMM合约涉及大量ETH/代币转账,必须加锁:

    solidity

    bool private locked;
    
    modifier noReentrant() {
        require(!locked, "Reentrancy detected");
        locked = true;
        _;
        locked = false;
    }
    

    2. 闪电贷风险

    简化版AMM无法防止闪电贷攻击。攻击者可以:

    1. 从池子借出大额资金
    2. 在其他交易所搬砖
    3. 把钱还回来

    Uniswap V2通过k = x * y必须在交易后不变来防止这个问题:

    solidity

    function _update(uint256 balance0, uint256 balance1) private {
        require(balance0 != 0 || balance1 != 0, "K invariant not satisfied");
        // 验证k不变(允许小误差)
    }
    

    3. 精度问题

    代币通常有不同的小数位数。在计算时必须考虑精度转换:

    solidity

    // 假设 tokenA 是 18 位小数,tokenB 是 6 位小数
    // 存储时统一转为 18 位精度
    uint256 public reserveA;  // 18位精度
    uint256 public reserveB;  // 转换为18位精度存储
    

    4. 权限控制

    addLiquidityremoveLiquidity函数需要考虑谁有权调用。完整的AMM合约通常会引入工厂合约模式,由工厂创建交易对:

    solidity

    // 工厂合约创建交易对
    function createPair(address tokenA, address tokenB) external returns (address pair) {
        // ...
    }
    

    进阶话题:Uniswap V2 vs V3

    Uniswap V3最大的创新是集中流动性(Concentrated Liquidity)

    传统V2中,LP的流动性均匀分布在整个价格曲线 [0, ∞) 上。但实际上,大多数交易都发生在某个特定价格区间。

    V3允许LP将流动性集中在特定价格段,比如 [1800, 2200]。这意味着相同资金量可以获得更高的手续费收益,但代价是当价格偏离该区间时,你的流动性就不再被使用——需要承担更高的无常损失风险。

    V3的数学公式变为:

    plaintext

    (x + Δx) * (y + Δy) = k  // 只在这个价格段内成立
    

    超出价格段时,该LP的资金会被完全换成一侧的资产,相当于”退出”了流动性池。

    总结

    AMM的核心原理其实很简洁:

    1. XY=K公式:交易前后恒定乘积不变
    2. 价格由池子深度决定:池子越大,滑点越小
    3. 无常损失是LP的主要风险:价格波动越大,损失越严重

    理解这些基础概念后,你可以进一步研究:

    • Uniswap V3的集中流动性机制
    • Curve Finance的StableSwap公式(适合稳定币交易对)
    • 自动做市商的安全漏洞和攻击向量

    DeFi的世界很精彩,数学和代码的结合创造出了无限可能。希望这篇文章能成为你深入学习的起点。

    相关推荐阅读: