feat: 完成 Mini Swap DEX AMM 项目开发

- 添加智能合约: TokenA, TokenB, MiniSwapAMM
- 实现 AMM 流动性池功能 (x * y = k 公式)
- 支持添加/移除流动性和代币交换
- 包含完整的测试套件
- 创建 React 前端界面,支持钱包连接
- 添加 Web3 集成和现代化 UI 设计
- 包含部署脚本和完整的项目配置
This commit is contained in:
2025-07-10 01:39:43 +08:00
parent 8a2454a950
commit 3faf89e0a1
38 changed files with 28192 additions and 1 deletions

1
contracts/.gitkeep Normal file
View File

@ -0,0 +1 @@
# 智能合约目录

239
contracts/MiniSwapAMM.sol Normal file
View File

@ -0,0 +1,239 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
/**
* @title MiniSwapAMM
* @dev 简单的自动做市商 (AMM) 实现,基于 x * y = k 公式
*/
contract MiniSwapAMM is ERC20, ReentrancyGuard {
IERC20 public immutable tokenA;
IERC20 public immutable tokenB;
uint256 public reserveA;
uint256 public reserveB;
uint256 private constant MINIMUM_LIQUIDITY = 10**3;
event AddLiquidity(
address indexed provider,
uint256 amountA,
uint256 amountB,
uint256 liquidity
);
event RemoveLiquidity(
address indexed provider,
uint256 amountA,
uint256 amountB,
uint256 liquidity
);
event Swap(
address indexed trader,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 amountOut
);
constructor(address _tokenA, address _tokenB) ERC20("MiniSwap LP Token", "MSLP") {
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
/**
* @dev 添加流动性
* @param amountADesired 期望添加的 tokenA 数量
* @param amountBDesired 期望添加的 tokenB 数量
* @param amountAMin 最小添加的 tokenA 数量
* @param amountBMin 最小添加的 tokenB 数量
* @return amountA 实际添加的 tokenA 数量
* @return amountB 实际添加的 tokenB 数量
* @return liquidity 获得的 LP 代币数量
*/
function addLiquidity(
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin
) external nonReentrant returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
(amountA, amountB) = _calculateOptimalAmounts(
amountADesired,
amountBDesired,
amountAMin,
amountBMin
);
// 转移代币到合约
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
liquidity = _mintLiquidity(amountA, amountB);
emit AddLiquidity(msg.sender, amountA, amountB, liquidity);
}
/**
* @dev 移除流动性
* @param liquidity 要移除的 LP 代币数量
* @param amountAMin 最小获得的 tokenA 数量
* @param amountBMin 最小获得的 tokenB 数量
* @return amountA 获得的 tokenA 数量
* @return amountB 获得的 tokenB 数量
*/
function removeLiquidity(
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin
) external nonReentrant returns (uint256 amountA, uint256 amountB) {
require(liquidity > 0, "MiniSwapAMM: INSUFFICIENT_LIQUIDITY");
uint256 totalSupply = totalSupply();
amountA = (liquidity * reserveA) / totalSupply;
amountB = (liquidity * reserveB) / totalSupply;
require(amountA >= amountAMin, "MiniSwapAMM: INSUFFICIENT_A_AMOUNT");
require(amountB >= amountBMin, "MiniSwapAMM: INSUFFICIENT_B_AMOUNT");
_burn(msg.sender, liquidity);
tokenA.transfer(msg.sender, amountA);
tokenB.transfer(msg.sender, amountB);
_updateReserves();
emit RemoveLiquidity(msg.sender, amountA, amountB, liquidity);
}
/**
* @dev 交换代币 A 到代币 B
* @param amountAIn 输入的 tokenA 数量
* @param amountBOutMin 最小输出的 tokenB 数量
* @return amountBOut 实际输出的 tokenB 数量
*/
function swapAForB(uint256 amountAIn, uint256 amountBOutMin)
external nonReentrant returns (uint256 amountBOut) {
require(amountAIn > 0, "MiniSwapAMM: INSUFFICIENT_INPUT_AMOUNT");
amountBOut = getAmountOut(amountAIn, reserveA, reserveB);
require(amountBOut >= amountBOutMin, "MiniSwapAMM: INSUFFICIENT_OUTPUT_AMOUNT");
tokenA.transferFrom(msg.sender, address(this), amountAIn);
tokenB.transfer(msg.sender, amountBOut);
_updateReserves();
emit Swap(msg.sender, address(tokenA), address(tokenB), amountAIn, amountBOut);
}
/**
* @dev 交换代币 B 到代币 A
* @param amountBIn 输入的 tokenB 数量
* @param amountAOutMin 最小输出的 tokenA 数量
* @return amountAOut 实际输出的 tokenA 数量
*/
function swapBForA(uint256 amountBIn, uint256 amountAOutMin)
external nonReentrant returns (uint256 amountAOut) {
require(amountBIn > 0, "MiniSwapAMM: INSUFFICIENT_INPUT_AMOUNT");
amountAOut = getAmountOut(amountBIn, reserveB, reserveA);
require(amountAOut >= amountAOutMin, "MiniSwapAMM: INSUFFICIENT_OUTPUT_AMOUNT");
tokenB.transferFrom(msg.sender, address(this), amountBIn);
tokenA.transfer(msg.sender, amountAOut);
_updateReserves();
emit Swap(msg.sender, address(tokenB), address(tokenA), amountBIn, amountAOut);
}
/**
* @dev 根据输入数量计算输出数量 (基于 x * y = k 公式)
* @param amountIn 输入数量
* @param reserveIn 输入代币储备量
* @param reserveOut 输出代币储备量
* @return amountOut 输出数量
*/
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
public pure returns (uint256 amountOut) {
require(amountIn > 0, "MiniSwapAMM: INSUFFICIENT_INPUT_AMOUNT");
require(reserveIn > 0 && reserveOut > 0, "MiniSwapAMM: INSUFFICIENT_LIQUIDITY");
// 考虑 0.3% 的交易费用
uint256 amountInWithFee = amountIn * 997;
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = (reserveIn * 1000) + amountInWithFee;
amountOut = numerator / denominator;
}
/**
* @dev 获取当前储备量
*/
function getReserves() external view returns (uint256 _reserveA, uint256 _reserveB) {
return (reserveA, reserveB);
}
/**
* @dev 计算最优的代币数量比例
*/
function _calculateOptimalAmounts(
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin
) internal view returns (uint256 amountA, uint256 amountB) {
if (reserveA == 0 && reserveB == 0) {
// 首次添加流动性
return (amountADesired, amountBDesired);
} else {
uint256 amountBOptimal = (amountADesired * reserveB) / reserveA;
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, "MiniSwapAMM: INSUFFICIENT_B_AMOUNT");
return (amountADesired, amountBOptimal);
} else {
uint256 amountAOptimal = (amountBDesired * reserveA) / reserveB;
require(amountAOptimal <= amountADesired && amountAOptimal >= amountAMin,
"MiniSwapAMM: INSUFFICIENT_A_AMOUNT");
return (amountAOptimal, amountBDesired);
}
}
}
/**
* @dev 铸造流动性代币
*/
function _mintLiquidity(uint256 amountA, uint256 amountB) internal returns (uint256 liquidity) {
uint256 totalSupply = totalSupply();
if (totalSupply == 0) {
// 首次添加流动性
liquidity = Math.sqrt(amountA * amountB);
require(liquidity > MINIMUM_LIQUIDITY, "MiniSwapAMM: INSUFFICIENT_LIQUIDITY_MINTED");
// 从流动性中减去最小流动性,永久锁定
liquidity = liquidity - MINIMUM_LIQUIDITY;
} else {
liquidity = Math.min(
(amountA * totalSupply) / reserveA,
(amountB * totalSupply) / reserveB
);
}
require(liquidity > 0, "MiniSwapAMM: INSUFFICIENT_LIQUIDITY_MINTED");
_mint(msg.sender, liquidity);
_updateReserves();
}
/**
* @dev 更新储备量
*/
function _updateReserves() internal {
reserveA = tokenA.balanceOf(address(this));
reserveB = tokenB.balanceOf(address(this));
}
}

34
contracts/TokenA.sol Normal file
View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TokenA
* @dev 用于 DEX 的第一个 ERC-20 代币
*/
contract TokenA is ERC20, Ownable {
constructor() ERC20("Token A", "TKA") Ownable(msg.sender) {
// 初始铸造 1,000,000 个代币给部署者
_mint(msg.sender, 1000000 * 10**decimals());
}
/**
* @dev 允许所有者铸造更多代币
* @param to 接收代币的地址
* @param amount 铸造的代币数量
*/
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
/**
* @dev 允许任何人铸造代币用于测试(仅限测试环境)
* @param amount 铸造的代币数量
*/
function faucet(uint256 amount) public {
require(amount <= 10000 * 10**decimals(), "TokenA: Maximum 10,000 tokens per faucet call");
_mint(msg.sender, amount);
}
}

34
contracts/TokenB.sol Normal file
View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TokenB
* @dev 用于 DEX 的第二个 ERC-20 代币
*/
contract TokenB is ERC20, Ownable {
constructor() ERC20("Token B", "TKB") Ownable(msg.sender) {
// 初始铸造 1,000,000 个代币给部署者
_mint(msg.sender, 1000000 * 10**decimals());
}
/**
* @dev 允许所有者铸造更多代币
* @param to 接收代币的地址
* @param amount 铸造的代币数量
*/
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
/**
* @dev 允许任何人铸造代币用于测试(仅限测试环境)
* @param amount 铸造的代币数量
*/
function faucet(uint256 amount) public {
require(amount <= 10000 * 10**decimals(), "TokenB: Maximum 10,000 tokens per faucet call");
_mint(msg.sender, amount);
}
}