深入浅出流动性池:手把手教你写出第一个去中心化做市商合约

68 阅读7分钟

前言

本文通过理论拆解与代码实战,完整演示了基于 OpenZeppelin V5 构建自动做市商(AMM)合约的全过程*。从恒定乘积公式的数学原理,到具备铸造功能的 ERC20 代币设计,再到使用 Hardhat 与 Viem 编写的自动化测试脚本,构建了一套开箱即用的 DeFi 最小可行性产品,助你快速掌握现代去中心化交易所的核心开发范式。

概述

  • 自动做市商(Automated Market Maker, AMM)  是去中心化交易所(DEX)的核心技术,它彻底改变了传统的资产交易方式。

  • AMM(流动性池模式):它不需要买卖双方即时匹配,而是将资产预先存入一个“资金池”。交易者是直接与智能合约(资金池)交易,价格由数学公式自动计算。

如何运作

AMM 使用 恒定乘积公式: 𝑥×𝑦=𝑘

  • xy 分别代表池子里两种代币的数量。

  • k 是一个固定常数。

  • 交易原理: 当你买入代币 X 时,池子里的 X 减少,为了保持乘积𝑘不变,代币 Y 的数量必须增加。因此,随着你买得越多,X 的价格就会自动上涨。

AMM 里的三大角色 

  1. 流动性提供者 (LP):  普通用户可以将自己的资产(如 ETH + USDT)存入池子,供他人交易。作为回报,LP 会按比例赚取交易手续费。
  2. 交易者:  随时通过流动性池兑换资产,无需等待撮合,即换即走。
  3. 套利者:  当 AMM 池内的价格与外部市场不一致时,套利者会进场低买高卖,直到价格回升到市场水平,从而维持价格准确性

优缺点 

  • 优点:

    • 持续流动性:  只要池里有钱,随时可以交易。
    • 低门槛:  任何人都可以通过提供流动性来赚取收益(“全民做市商”)。
    • 去中心化:  无需中心化机构,代码即法律。
  • 风险:

    • 无常损失 (Impermanent Loss):  当池内资产价格剧烈波动时,LP 存入资产的价值可能低于直接持有这些资产的价值。
    • 滑点:  如果交易金额巨大或池子资金太小,成交价格会偏离预期价格。

智能合约开发、测试、部署

指令汇总

  1. 编译指令npx hardhat compile
  2. 部署指令npx hardhat run ./scripts/xxx.ts
  3. 测试指令npx hardhat test ./test/xxx.ts

智能合约

1.代币合约

说明:具备铸造功能的Token,初始值:1000000MTK,基于openzeppelin中的ERC20标准

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract BoykaYuriToken is ERC20, ERC20Burnable, Ownable, ERC20Permit {
    constructor(address recipient, address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable(initialOwner)
        ERC20Permit("MyToken")
    {
        _mint(recipient, 1000000 * 10 ** decimals());
    }
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

2.做市商合约

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

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

/**
 * @title SimpleDEX
 * @dev 基于 OpenZeppelin V5 的极简 ETH-Token 交易对合约
 */
contract SimpleDEX is ERC20 {
    using SafeERC20 for IERC20;

    IERC20 public immutable token;

    event LiquidityAdded(address provider, uint256 tokenAmount, uint256 ethAmount);
    event LiquidityRemoved(address provider, uint256 tokenAmount, uint256 ethAmount);
    event Swap(address user, uint256 inputAmount, uint256 outputAmount, bool ethToToken);

    constructor(address _token) ERC20("DEX LP Token", "DEX-LP") {
        token = IERC20(_token);
    }

    // --- 流动性管理 ---

    /**
     * @dev 添加流动性。初次添加确定比例,后续按比例注入。
     */
    function addLiquidity(uint256 _tokenAmount) external payable returns (uint256 lpTokens) {
        uint256 ethReserve = address(this).balance - msg.value;
        uint256 tokenReserve = token.balanceOf(address(this));

        if (tokenReserve == 0) {
            // 初次添加,LP 数量等于 ETH 数量
            lpTokens = msg.value;
        } else {
            // 按比例计算:(msg.value / ethReserve) * totalSupply
            lpTokens = (msg.value * totalSupply()) / ethReserve;
            // 确保代币投入量符合比例
            uint256 requiredToken = (msg.value * tokenReserve) / ethReserve;
            require(_tokenAmount >= requiredToken, "Insufficient token amount");
            _tokenAmount = requiredToken;
        }

        token.safeTransferFrom(msg.sender, address(this), _tokenAmount);
        _mint(msg.sender, lpTokens);
        
        emit LiquidityAdded(msg.sender, _tokenAmount, msg.value);
    }

    /**
     * @dev 移除流动性并销毁 LP 代币
     */
    function removeLiquidity(uint256 _lpAmount) external returns (uint256 ethAmount, uint256 tokenAmount) {
        require(_lpAmount > 0, "Invalid amount");
        
        uint256 ethReserve = address(this).balance;
        uint256 tokenReserve = token.balanceOf(address(this));
        uint256 totalLP = totalSupply();

        ethAmount = (_lpAmount * ethReserve) / totalLP;
        tokenAmount = (_lpAmount * tokenReserve) / totalLP;

        _burn(msg.sender, _lpAmount);
        payable(msg.sender).transfer(ethAmount);
        token.safeTransfer(msg.sender, tokenAmount);

        emit LiquidityRemoved(msg.sender, tokenAmount, ethAmount);
    }

    // --- 交易功能 ---

    /**
     * @dev 计算输出金额 (x * y = k)
     * 公式: out = (in * reserveOut) / (reserveIn + in)
     */
    function getAmountOut(uint256 _inputAmount, uint256 _inputReserve, uint256 _outputReserve) public pure returns (uint256) {
        // 包含 0.3% 手续费 (乘以 997 / 1000)
        uint256 inputWithFee = _inputAmount * 997;
        uint256 numerator = inputWithFee * _outputReserve;
        uint256 denominator = (_inputReserve * 1000) + inputWithFee;
        return numerator / denominator;
    }

    function ethToToken() external payable {
        uint256 tokenReserve = token.balanceOf(address(this));
        uint256 tokensBought = getAmountOut(msg.value, address(this).balance - msg.value, tokenReserve);
        
        token.safeTransfer(msg.sender, tokensBought);
        emit Swap(msg.sender, msg.value, tokensBought, true);
    }

    function tokenToEth(uint256 _tokenSold) external {
        uint256 tokenReserve = token.balanceOf(address(this));
        uint256 ethBought = getAmountOut(_tokenSold, tokenReserve, address(this).balance);
        
        token.safeTransferFrom(msg.sender, address(this), _tokenSold);
        payable(msg.sender).transfer(ethBought);
        emit Swap(msg.sender, _tokenSold, ethBought, false);
    }
}

部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const BoykaYuriTokenArtifact = await artifacts.readArtifact("BoykaYuriToken");
  const SimpleDEXArtifact = await artifacts.readArtifact("SimpleDEX");
 
  // 部署(构造函数参数:recipient, initialOwner)
  const BoykaYuriTokenHash = await deployer.deployContract({
    abi: BoykaYuriTokenArtifact.abi,//获取abi
    bytecode: BoykaYuriTokenArtifact.bytecode,//硬编码
    args: [deployerAddress,deployerAddress],//部署者地址,初始所有者地址
  });
   const BoykaYuriTokenReceipt = await publicClient.waitForTransactionReceipt({ hash: BoykaYuriTokenHash });
   console.log("代币合约地址:", BoykaYuriTokenReceipt.contractAddress);
//
const SimpleDEXHash = await deployer.deployContract({
    abi: SimpleDEXArtifact.abi,//获取abi
    bytecode: SimpleDEXArtifact.bytecode,//硬编码
    args: [BoykaYuriTokenReceipt.contractAddress],//部署者地址,初始所有者地址
  });
  // 等待确认并打印地址
  const SimpleDEXReceipt = await publicClient.waitForTransactionReceipt({ hash: SimpleDEXHash });
  console.log("交易所合约地址:", SimpleDEXReceipt.contractAddress);
}

main().catch(console.error);

测试脚本

import assert from "node:assert/strict";
import { describe, it,beforeEach } from "node:test";
import { parseEther, parseEventLogs,formatEther } from 'viem';
import { network } from "hardhat";

describe("SimpleDEX", function () {
  let Token: any,  DEX: any;
  let publicClient: any;
  let owner: any, user1: any, user2: any, user3: any;
  let deployerAddress: string;
  // const INITIAL_SUPPLY = parseEther("10000"); // 10,000 Tokens

  beforeEach(async function () {
     const { viem } = await network.connect();
      publicClient = await viem.getPublicClient();//创建一个公共客户端实例用于读取链上数据(无需私钥签名)。
      [owner, user1] = await viem.getWalletClients();
      deployerAddress = owner.account.address;//钱包地址
      // 1. 部署代币合约
      Token= await viem.deployContract("BoykaYuriToken", [
                    deployerAddress,
                    deployerAddress
                ]);//部署合约
      console.log("部署的代币合约地址:", await Token.address);
    // 2. 部署交易所
      DEX = await viem.deployContract("SimpleDEX", [
            await Token.address,
      ]);//部署合约
      console.log("部署的交易所合约地址:", await DEX.address);
  });
  describe("Liquidity", function () {
    it("应该能成功添加流动性", async function () {
      const tokenAmount = parseEther("100");
      const ethAmount = parseEther("1");
      // 授权 DEX 动用代币
      const approveTx = await Token.write.approve([await DEX.address, tokenAmount]);
    await publicClient.waitForTransactionReceipt({ hash: approveTx });
    // 添加流动性
    const addLiquidityTx = await DEX.write.addLiquidity([tokenAmount], {
      value: ethAmount,
    });
    const receipt = await publicClient.waitForTransactionReceipt({ hash: addLiquidityTx });
    // 检查余额
    const dexTokenBalance = await Token.read.balanceOf([await DEX.address]);
    assert.equal(dexTokenBalance, tokenAmount);
    console.log("交易所中的代币余额:",formatEther(dexTokenBalance), "MTK")
    console.log("交易所中的 ETH 余额:",formatEther(ethAmount), "ETH")
    // ETH 余额 (使用 publicClient 直接查询)
    const dexEthBalance = await publicClient.getBalance({ address: DEX.address });
    // expect(dexEthBalance).to.equal(ethAmount);
    console.log("交易所中的 ETH 余额:",formatEther(dexEthBalance), "ETH")
    // LP 代币余额
    const ownerLPBalance = await DEX.read.balanceOf([owner.account.address]);
    console.log("用户 LP 代币余额:",formatEther(ownerLPBalance), "LP")
    });
  });

  describe("Swaps", function () {
    beforeEach(async function () {
      // 初始注入:1000 Token / 10 ETH (比例 100:1)
      const tokenAmount = parseEther("1000");
      const ethAmount = parseEther("10");
      await Token.write.approve([await DEX.address, tokenAmount]);
      await DEX.write.addLiquidity([tokenAmount], {
        value: ethAmount,
      });
    });

    it("ETH 换 Token 应该符合 x*y=k 公式", async function () {
      const ethIn = parseEther("1");

    // 使用 addr1 执行兑换 (通过 account 参数指定调用者)
    const hash = await DEX.write.ethToToken({ 
      value: ethIn,
      account: user1.account // 指定 addr1 发起交易
    });
    await publicClient.waitForTransactionReceipt({ hash });

    // 查询余额 (read)
    const addr1TokenBalance = await Token.read.balanceOf([user1.account.address]);
    console.log("1 ETH 换得的代币数量:", formatEther(addr1TokenBalance));

    // 预期计算: 约 90.66 Tokens
    console.log(formatEther(addr1TokenBalance))
  })
    it("Token 换 ETH 应该符合公式", async function () {
      const tokenIn = parseEther("100");
      
      // 先给 user1 一些代币
      await Token.write.transfer([user1.account.address, tokenIn]);
      // user1 授权并兑换
      await Token.write.approve([await DEX.address, tokenIn], {
        account: user1.account,
      });
      // user1 兑换 ETH
      const initialEth = await publicClient.getBalance({ address: user1.account.address });
      await DEX.write.tokenToEth([tokenIn], {
        account: user1.account,
      });
      // user1 检查 ETH 余额
      const finalEth = await publicClient.getBalance({ address: user1.account.address });
      console.log(formatEther(initialEth), formatEther(finalEth))
      console.log("100 Tokens 换得的 ETH 数量:", formatEther(finalEth - initialEth), "ETH");
    });
  });
});

总结

至此,自动做市商(AMM)的理论知识梳理与代码实现已全部完成,涵盖开发、测试、部署全流程。 本次工作既厘清了AMM核心逻辑与价值,也落地了全流程技术实现,明确了后续优化方向。 后续可基于本次全流程成果,聚焦技术优化与合规落地,助力AMM在DeFi生态稳健应用。