DeFi 智能合约实战:AMM 自动做市商(Uniswap-like)

AMM(Automated Market Maker)是去中心化金融(DeFi)中的核心机制,它通过算法自动提供流动性,实现代币交换。Uniswap 是最著名的 AMM 实现,基于恒定乘积公式(Constant Product Formula)。本文将逐步讲解其原理和智能合约实战实现,包括数学基础、Solidity 代码示例和关键注意事项。所有内容基于真实可靠的 Uniswap V1/V2 设计,确保实用性。

1. AMM 基础概念

AMM 的核心是流动性池(Liquidity Pool),用户存入两种代币(例如 ETH 和 DAI)作为储备。交易者通过池子进行交换,而不依赖传统订单簿。关键机制:

  • 流动性提供者(LP):用户添加代币到池子,获得 LP 代币作为份额证明。
  • 交易者:输入一种代币,输出另一种代币,价格由算法自动计算。
  • 恒定乘积公式:这是 Uniswap 的核心,确保池子总价值不变。公式为: $$ x \cdot y = k $$ 其中:
    • $x$ 和 $y$ 是池中两种代币的储备量。
    • $k$ 是常数,由初始流动性决定。 例如,当用户输入 $\Delta x$ 个代币 A 时,输出 $\Delta y$ 个代币 B 满足: $$ (x + \Delta x) \cdot (y - \Delta y) = k $$ 解出 $\Delta y$: $$ \Delta y = y - \frac{k}{x + \Delta x} $$ 这确保了滑点(价格变动)随交易量增大而增加。
2. 智能合约实战实现

以下是一个简化版的 Uniswap-like AMM 智能合约,用 Solidity 编写。合约包含核心功能:添加流动性、移除流动性、交换代币。我们假设代币为 ERC-20 标准(需先部署代币合约)。

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

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

contract SimpleAMM {
    // 代币地址
    IERC20 public tokenA;
    IERC20 public tokenB;
    
    // 储备量
    uint public reserveA;
    uint public reserveB;
    
    // LP 代币总量(简化处理,实际 Uniswap 使用 ERC-20 LP 代币)
    uint public totalLPTokens;
    mapping(address => uint) public lpBalances;
    
    // 事件
    event LiquidityAdded(address indexed provider, uint amountA, uint amountB, uint lpTokens);
    event LiquidityRemoved(address indexed provider, uint amountA, uint amountB, uint lpTokens);
    event Swap(address indexed trader, uint amountIn, uint amountOut, bool isAToB);
    
    constructor(address _tokenA, address _tokenB) {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }
    
    // 添加流动性:用户存入等值代币,获得 LP 代币
    function addLiquidity(uint amountA, uint amountB) external {
        require(amountA > 0 && amountB > 0, "Amounts must be positive");
        // 计算 k 值(首次添加时初始化)
        if (reserveA == 0 || reserveB == 0) {
            reserveA = amountA;
            reserveB = amountB;
            totalLPTokens = sqrt(amountA * amountB); // 简化:LP 代币总量为 sqrt(x*y)
            lpBalances[msg.sender] = totalLPTokens;
        } else {
            // 检查比例是否匹配(实际 Uniswap 允许一定偏差)
            require(amountA * reserveB == amountB * reserveA, "Invalid ratio");
            // 更新储备
            reserveA += amountA;
            reserveB += amountB;
            // 计算 LP 代币奖励:基于新增流动性比例
            uint newLPTokens = (amountA * totalLPTokens) / reserveA;
            totalLPTokens += newLPTokens;
            lpBalances[msg.sender] += newLPTokens;
        }
        // 转移代币到合约
        tokenA.transferFrom(msg.sender, address(this), amountA);
        tokenB.transferFrom(msg.sender, address(this), amountB);
        emit LiquidityAdded(msg.sender, amountA, amountB, lpBalances[msg.sender]);
    }
    
    // 移除流动性:用户销毁 LP 代币,取回代币
    function removeLiquidity(uint lpTokens) external {
        require(lpTokens > 0 && lpTokens <= lpBalances[msg.sender], "Invalid LP tokens");
        // 计算可取回代币量
        uint amountA = (lpTokens * reserveA) / totalLPTokens;
        uint amountB = (lpTokens * reserveB) / totalLPTokens;
        // 更新状态
        reserveA -= amountA;
        reserveB -= amountB;
        totalLPTokens -= lpTokens;
        lpBalances[msg.sender] -= lpTokens;
        // 转移代币给用户
        tokenA.transfer(msg.sender, amountA);
        tokenB.transfer(msg.sender, amountB);
        emit LiquidityRemoved(msg.sender, amountA, amountB, lpTokens);
    }
    
    // 交换代币:输入一种代币,输出另一种
    function swap(uint amountIn, bool isAToB) external {
        require(amountIn > 0, "Amount must be positive");
        uint amountOut;
        if (isAToB) {
            // 计算输出量:基于公式 Δy = y - k/(x + Δx)
            amountOut = reserveB - (reserveA * reserveB) / (reserveA + amountIn);
            // 更新储备
            reserveA += amountIn;
            reserveB -= amountOut;
            // 转移代币
            tokenA.transferFrom(msg.sender, address(this), amountIn);
            tokenB.transfer(msg.sender, amountOut);
        } else {
            // 反向交换
            amountOut = reserveA - (reserveA * reserveB) / (reserveB + amountIn);
            reserveB += amountIn;
            reserveA -= amountOut;
            tokenB.transferFrom(msg.sender, address(this), amountIn);
            tokenA.transfer(msg.sender, amountOut);
        }
        emit Swap(msg.sender, amountIn, amountOut, isAToB);
    }
    
    // 辅助函数:计算平方根(简化版,实际用库如 SafeMath)
    function sqrt(uint x) internal pure returns (uint y) {
        uint z = (x + 1) / 2;
        y = x;
        while (z < y) {
            y = z;
            z = (x / z + z) / 2;
        }
    }
}

3. 代码关键点解释
  • 数学原理实现
    • 恒定乘积公式 $x \cdot y = k$ 在 swap 函数中体现:计算 $\Delta y$ 时使用 $\Delta y = y - \frac{k}{x + \Delta x}$。
    • 添加流动性时,LP 代币计算基于流动性比例,例如 $ \text{newLPTokens} = \frac{\Delta x \cdot \text{totalLPTokens}}{x} $。
  • 核心函数
    • addLiquidity:用户存入两种代币,合约检查比例(首次添加时初始化池子),并发行 LP 代币。
    • removeLiquidity:用户销毁 LP 代币,按比例取回代币。
    • swap:用户输入代币 A 或 B,输出另一种代币。公式确保 $k$ 不变,防止池子耗尽。
  • 安全设计
    • 使用 OpenZeppelin 的 IERC20 确保代币交互安全。
    • 检查输入量大于零,防止无效交易。
    • 事件日志(emit)用于跟踪操作。
4. 注意事项和最佳实践
  • 安全风险:AMM 合约易受攻击,如重入攻击(实际中应使用 Checks-Effects-Interactions 模式)和价格操纵。建议:
    • 添加滑点保护(例如,设置最小输出量)。
    • 使用成熟的库(如 Uniswap V2 的 Router 合约处理复杂逻辑)。
  • Gas 优化:简化版代码未优化 Gas,实际项目应使用内联汇编或状态变量缓存。
  • 测试和部署:在测试网(如 Rinkeby)上测试所有场景,使用工具如 Hardhat 和 Truffle。
  • 扩展功能:真实 Uniswap 添加了手续费(例如 0.3% 交易费),可在 swap 函数中实现:输出量减少一定比例,奖励给 LP。

通过这个实战示例,您可以理解 AMM 的核心机制并部署自己的合约。实际开发中,参考 Uniswap 开源代码(GitHub)以获取更完整实现。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐