前言
本文通过理论拆解与代码实战,完整演示了基于 OpenZeppelin V5 构建自动做市商(AMM)合约的全过程*。从恒定乘积公式的数学原理,到具备铸造功能的 ERC20 代币设计,再到使用 Hardhat 与 Viem 编写的自动化测试脚本,构建了一套开箱即用的 DeFi 最小可行性产品,助你快速掌握现代去中心化交易所的核心开发范式。
概述
-
自动做市商(Automated Market Maker, AMM) 是去中心化交易所(DEX)的核心技术,它彻底改变了传统的资产交易方式。
-
AMM(流动性池模式):它不需要买卖双方即时匹配,而是将资产预先存入一个“资金池”。交易者是直接与智能合约(资金池)交易,价格由数学公式自动计算。
如何运作
AMM 使用 恒定乘积公式: 𝑥×𝑦=𝑘
-
x 和 y 分别代表池子里两种代币的数量。
-
k 是一个固定常数。
-
交易原理: 当你买入代币 X 时,池子里的 X 减少,为了保持乘积𝑘不变,代币 Y 的数量必须增加。因此,随着你买得越多,X 的价格就会自动上涨。
AMM 里的三大角色
- 流动性提供者 (LP): 普通用户可以将自己的资产(如 ETH + USDT)存入池子,供他人交易。作为回报,LP 会按比例赚取交易手续费。
- 交易者: 随时通过流动性池兑换资产,无需等待撮合,即换即走。
- 套利者: 当 AMM 池内的价格与外部市场不一致时,套利者会进场低买高卖,直到价格回升到市场水平,从而维持价格准确性
优缺点
-
优点:
- 持续流动性: 只要池里有钱,随时可以交易。
- 低门槛: 任何人都可以通过提供流动性来赚取收益(“全民做市商”)。
- 去中心化: 无需中心化机构,代码即法律。
-
风险:
- 无常损失 (Impermanent Loss): 当池内资产价格剧烈波动时,LP 存入资产的价值可能低于直接持有这些资产的价值。
- 滑点: 如果交易金额巨大或池子资金太小,成交价格会偏离预期价格。
智能合约开发、测试、部署
指令汇总
- 编译指令:
npx hardhat compile - 部署指令:
npx hardhat run ./scripts/xxx.ts - 测试指令:
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生态稳健应用。