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

  • 木西
  • 发布于 1天前
  • 阅读 28

前言本文通过理论拆解与代码实战,完整演示了基于OpenZeppelinV5构建自动做市商(AMM)合约的全过程*。从恒定乘积公式的数学原理,到具备铸造功能的ERC20代币设计,再到使用Hardhat与Viem编写的自动化测试脚本,构建了一套开箱即用的DeFi最小可行性产品,

前言

本文通过理论拆解与代码实战,完整演示了基于 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生态稳健应用。
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。