feat: 完成 Mini Swap DEX AMM 项目开发
- 添加智能合约: TokenA, TokenB, MiniSwapAMM - 实现 AMM 流动性池功能 (x * y = k 公式) - 支持添加/移除流动性和代币交换 - 包含完整的测试套件 - 创建 React 前端界面,支持钱包连接 - 添加 Web3 集成和现代化 UI 设计 - 包含部署脚本和完整的项目配置
This commit is contained in:
283
test/MiniSwapAMM.test.js
Normal file
283
test/MiniSwapAMM.test.js
Normal file
@ -0,0 +1,283 @@
|
||||
const { expect } = require("chai");
|
||||
const { ethers } = require("hardhat");
|
||||
|
||||
describe("MiniSwapAMM", function () {
|
||||
let TokenA, TokenB, MiniSwapAMM;
|
||||
let tokenA, tokenB, miniSwapAMM;
|
||||
let owner, addr1, addr2;
|
||||
|
||||
const INITIAL_SUPPLY = ethers.parseEther("1000000");
|
||||
const LIQUIDITY_AMOUNT_A = ethers.parseEther("1000");
|
||||
const LIQUIDITY_AMOUNT_B = ethers.parseEther("2000");
|
||||
|
||||
beforeEach(async function () {
|
||||
[owner, addr1, addr2] = await ethers.getSigners();
|
||||
|
||||
// 部署代币合约
|
||||
TokenA = await ethers.getContractFactory("TokenA");
|
||||
tokenA = await TokenA.deploy();
|
||||
await tokenA.waitForDeployment();
|
||||
|
||||
TokenB = await ethers.getContractFactory("TokenB");
|
||||
tokenB = await TokenB.deploy();
|
||||
await tokenB.waitForDeployment();
|
||||
|
||||
// 部署 AMM 合约
|
||||
MiniSwapAMM = await ethers.getContractFactory("MiniSwapAMM");
|
||||
miniSwapAMM = await MiniSwapAMM.deploy(
|
||||
await tokenA.getAddress(),
|
||||
await tokenB.getAddress()
|
||||
);
|
||||
await miniSwapAMM.waitForDeployment();
|
||||
|
||||
// 给测试账户铸造代币
|
||||
await tokenA.connect(addr1).faucet(ethers.parseEther("10000"));
|
||||
await tokenB.connect(addr1).faucet(ethers.parseEther("10000"));
|
||||
await tokenA.connect(addr2).faucet(ethers.parseEther("10000"));
|
||||
await tokenB.connect(addr2).faucet(ethers.parseEther("10000"));
|
||||
});
|
||||
|
||||
describe("部署", function () {
|
||||
it("应该正确设置代币地址", async function () {
|
||||
expect(await miniSwapAMM.tokenA()).to.equal(await tokenA.getAddress());
|
||||
expect(await miniSwapAMM.tokenB()).to.equal(await tokenB.getAddress());
|
||||
});
|
||||
|
||||
it("应该正确设置 LP 代币信息", async function () {
|
||||
expect(await miniSwapAMM.name()).to.equal("MiniSwap LP Token");
|
||||
expect(await miniSwapAMM.symbol()).to.equal("MSLP");
|
||||
});
|
||||
|
||||
it("初始储备应该为 0", async function () {
|
||||
const [reserveA, reserveB] = await miniSwapAMM.getReserves();
|
||||
expect(reserveA).to.equal(0);
|
||||
expect(reserveB).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("添加流动性", function () {
|
||||
it("应该能够添加初始流动性", async function () {
|
||||
// 授权 AMM 合约使用代币
|
||||
await tokenA.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_A);
|
||||
await tokenB.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_B);
|
||||
|
||||
// 添加流动性
|
||||
const tx = await miniSwapAMM.connect(addr1).addLiquidity(
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B,
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B
|
||||
);
|
||||
|
||||
// 检查事件
|
||||
await expect(tx).to.emit(miniSwapAMM, "AddLiquidity")
|
||||
.withArgs(addr1.address, LIQUIDITY_AMOUNT_A, LIQUIDITY_AMOUNT_B, anyValue);
|
||||
|
||||
// 检查储备
|
||||
const [reserveA, reserveB] = await miniSwapAMM.getReserves();
|
||||
expect(reserveA).to.equal(LIQUIDITY_AMOUNT_A);
|
||||
expect(reserveB).to.equal(LIQUIDITY_AMOUNT_B);
|
||||
|
||||
// 检查 LP 代币余额
|
||||
const lpBalance = await miniSwapAMM.balanceOf(addr1.address);
|
||||
expect(lpBalance).to.be.gt(0);
|
||||
});
|
||||
|
||||
it("应该能够添加后续流动性", async function () {
|
||||
// 首先添加初始流动性
|
||||
await tokenA.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_A);
|
||||
await tokenB.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_B);
|
||||
await miniSwapAMM.connect(addr1).addLiquidity(
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B,
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B
|
||||
);
|
||||
|
||||
// 添加更多流动性
|
||||
const additionalA = ethers.parseEther("500");
|
||||
const additionalB = ethers.parseEther("1000");
|
||||
|
||||
await tokenA.connect(addr2).approve(await miniSwapAMM.getAddress(), additionalA);
|
||||
await tokenB.connect(addr2).approve(await miniSwapAMM.getAddress(), additionalB);
|
||||
|
||||
await miniSwapAMM.connect(addr2).addLiquidity(
|
||||
additionalA,
|
||||
additionalB,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
// 检查储备增加
|
||||
const [reserveA, reserveB] = await miniSwapAMM.getReserves();
|
||||
expect(reserveA).to.equal(LIQUIDITY_AMOUNT_A + additionalA);
|
||||
expect(reserveB).to.equal(LIQUIDITY_AMOUNT_B + additionalB);
|
||||
});
|
||||
});
|
||||
|
||||
describe("移除流动性", function () {
|
||||
beforeEach(async function () {
|
||||
// 添加初始流动性
|
||||
await tokenA.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_A);
|
||||
await tokenB.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_B);
|
||||
await miniSwapAMM.connect(addr1).addLiquidity(
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B,
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B
|
||||
);
|
||||
});
|
||||
|
||||
it("应该能够移除流动性", async function () {
|
||||
const lpBalance = await miniSwapAMM.balanceOf(addr1.address);
|
||||
const halfLiquidity = lpBalance / 2n;
|
||||
|
||||
const tx = await miniSwapAMM.connect(addr1).removeLiquidity(
|
||||
halfLiquidity,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
// 检查事件
|
||||
await expect(tx).to.emit(miniSwapAMM, "RemoveLiquidity");
|
||||
|
||||
// 检查 LP 代币余额减少
|
||||
const newLpBalance = await miniSwapAMM.balanceOf(addr1.address);
|
||||
expect(newLpBalance).to.equal(lpBalance - halfLiquidity);
|
||||
});
|
||||
|
||||
it("移除流动性时应该按比例返回代币", async function () {
|
||||
const lpBalance = await miniSwapAMM.balanceOf(addr1.address);
|
||||
|
||||
const balanceABefore = await tokenA.balanceOf(addr1.address);
|
||||
const balanceBBefore = await tokenB.balanceOf(addr1.address);
|
||||
|
||||
await miniSwapAMM.connect(addr1).removeLiquidity(
|
||||
lpBalance,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
const balanceAAfter = await tokenA.balanceOf(addr1.address);
|
||||
const balanceBAfter = await tokenB.balanceOf(addr1.address);
|
||||
|
||||
// 检查代币返还(接近初始流动性金额,因为有最小流动性锁定)
|
||||
expect(balanceAAfter - balanceABefore).to.be.closeTo(LIQUIDITY_AMOUNT_A, ethers.parseEther("1"));
|
||||
expect(balanceBAfter - balanceBBefore).to.be.closeTo(LIQUIDITY_AMOUNT_B, ethers.parseEther("1"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("代币交换", function () {
|
||||
beforeEach(async function () {
|
||||
// 添加流动性
|
||||
await tokenA.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_A);
|
||||
await tokenB.connect(addr1).approve(await miniSwapAMM.getAddress(), LIQUIDITY_AMOUNT_B);
|
||||
await miniSwapAMM.connect(addr1).addLiquidity(
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B,
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B
|
||||
);
|
||||
});
|
||||
|
||||
it("应该能够用 TokenA 交换 TokenB", async function () {
|
||||
const swapAmount = ethers.parseEther("100");
|
||||
|
||||
// 计算预期输出
|
||||
const expectedOutput = await miniSwapAMM.getAmountOut(
|
||||
swapAmount,
|
||||
LIQUIDITY_AMOUNT_A,
|
||||
LIQUIDITY_AMOUNT_B
|
||||
);
|
||||
|
||||
await tokenA.connect(addr2).approve(await miniSwapAMM.getAddress(), swapAmount);
|
||||
|
||||
const balanceBBefore = await tokenB.balanceOf(addr2.address);
|
||||
|
||||
const tx = await miniSwapAMM.connect(addr2).swapAForB(swapAmount, 0);
|
||||
|
||||
// 检查事件
|
||||
await expect(tx).to.emit(miniSwapAMM, "Swap");
|
||||
|
||||
const balanceBAfter = await tokenB.balanceOf(addr2.address);
|
||||
const actualOutput = balanceBAfter - balanceBBefore;
|
||||
|
||||
expect(actualOutput).to.equal(expectedOutput);
|
||||
});
|
||||
|
||||
it("应该能够用 TokenB 交换 TokenA", async function () {
|
||||
const swapAmount = ethers.parseEther("200");
|
||||
|
||||
const expectedOutput = await miniSwapAMM.getAmountOut(
|
||||
swapAmount,
|
||||
LIQUIDITY_AMOUNT_B,
|
||||
LIQUIDITY_AMOUNT_A
|
||||
);
|
||||
|
||||
await tokenB.connect(addr2).approve(await miniSwapAMM.getAddress(), swapAmount);
|
||||
|
||||
const balanceABefore = await tokenA.balanceOf(addr2.address);
|
||||
|
||||
await miniSwapAMM.connect(addr2).swapBForA(swapAmount, 0);
|
||||
|
||||
const balanceAAfter = await tokenA.balanceOf(addr2.address);
|
||||
const actualOutput = balanceAAfter - balanceABefore;
|
||||
|
||||
expect(actualOutput).to.equal(expectedOutput);
|
||||
});
|
||||
|
||||
it("交换应该改变储备比例", async function () {
|
||||
const swapAmount = ethers.parseEther("100");
|
||||
|
||||
const [reserveABefore, reserveBBefore] = await miniSwapAMM.getReserves();
|
||||
|
||||
await tokenA.connect(addr2).approve(await miniSwapAMM.getAddress(), swapAmount);
|
||||
await miniSwapAMM.connect(addr2).swapAForB(swapAmount, 0);
|
||||
|
||||
const [reserveAAfter, reserveBAfter] = await miniSwapAMM.getReserves();
|
||||
|
||||
expect(reserveAAfter).to.be.gt(reserveABefore);
|
||||
expect(reserveBAfter).to.be.lt(reserveBBefore);
|
||||
});
|
||||
|
||||
it("应该拒绝输出不足的交换", async function () {
|
||||
const swapAmount = ethers.parseEther("100");
|
||||
const minOutput = ethers.parseEther("1000"); // 设置过高的最小输出
|
||||
|
||||
await tokenA.connect(addr2).approve(await miniSwapAMM.getAddress(), swapAmount);
|
||||
|
||||
await expect(
|
||||
miniSwapAMM.connect(addr2).swapAForB(swapAmount, minOutput)
|
||||
).to.be.revertedWith("MiniSwapAMM: INSUFFICIENT_OUTPUT_AMOUNT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("价格计算", function () {
|
||||
it("getAmountOut 应该正确计算输出数量", async function () {
|
||||
const amountIn = ethers.parseEther("100");
|
||||
const reserveIn = ethers.parseEther("1000");
|
||||
const reserveOut = ethers.parseEther("2000");
|
||||
|
||||
const amountOut = await miniSwapAMM.getAmountOut(amountIn, reserveIn, reserveOut);
|
||||
|
||||
// 手动计算预期值:(100 * 997 * 2000) / (1000 * 1000 + 100 * 997)
|
||||
const amountInWithFee = amountIn * 997n;
|
||||
const numerator = amountInWithFee * reserveOut;
|
||||
const denominator = reserveIn * 1000n + amountInWithFee;
|
||||
const expected = numerator / denominator;
|
||||
|
||||
expect(amountOut).to.equal(expected);
|
||||
});
|
||||
|
||||
it("应该在没有流动性时拒绝计算", async function () {
|
||||
const amountIn = ethers.parseEther("100");
|
||||
|
||||
await expect(
|
||||
miniSwapAMM.getAmountOut(amountIn, 0, 1000)
|
||||
).to.be.revertedWith("MiniSwapAMM: INSUFFICIENT_LIQUIDITY");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 用于检查任意值的辅助函数
|
||||
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
|
||||
Reference in New Issue
Block a user