- 添加智能合约: TokenA, TokenB, MiniSwapAMM - 实现 AMM 流动性池功能 (x * y = k 公式) - 支持添加/移除流动性和代币交换 - 包含完整的测试套件 - 创建 React 前端界面,支持钱包连接 - 添加 Web3 集成和现代化 UI 设计 - 包含部署脚本和完整的项目配置
283 lines
9.9 KiB
JavaScript
283 lines
9.9 KiB
JavaScript
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");
|