前言本文通过理论拆解与代码实战,完整演示了基于OpenZeppelinV5构建自动做市商(AMM)合约的全过程*。从恒定乘积公式的数学原理,到具备铸造功能的ERC20代币设计,再到使用Hardhat与Viem编写的自动化测试脚本,构建了一套开箱即用的DeFi最小可行性产品,
本文通过理论拆解与代码实战,完整演示了基于 OpenZeppelin V5 构建自动做市商(AMM)合约的全过程*。从恒定乘积公式的数学原理,到具备铸造功能的 ERC20 代币设计,再到使用 Hardhat 与 Viem 编写的自动化测试脚本,构建了一套开箱即用的 DeFi 最小可行性产品,助你快速掌握现代去中心化交易所的核心开发范式。
概述
- 自动做市商(Automated Market Maker, AMM) 是去中心化交易所(DEX)的核心技术,它彻底改变了传统的资产交易方式。
AMM(流动性池模式):它不需要买卖双方即时匹配,而是将资产预先存入一个“资金池”。交易者是直接与智能合约(资金池)交易,价格由数学公式自动计算。
AMM 使用 恒定乘积公式: 𝑥×𝑦=𝑘
x 和 y 分别代表池子里两种代币的数量。
k 是一个固定常数。
交易原理: 当你买入代币 X 时,池子里的 X 减少,为了保持乘积𝑘不变,代币 Y 的数量必须增加。因此,随着你买得越多,X 的价格就会自动上涨。
优点:
风险:
npx hardhat compilenpx hardhat run ./scripts/xxx.tsnpx hardhat test ./test/xxx.ts
说明:具备铸造功能的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";
/**
@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); }
// --- 交易功能 ---
/**
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生态稳健应用。 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!