- 新增 CLI 工具,支持通过命令行与 MiniSwapAMM 合约交互 - 创建 CLI 使用指南文档,详细说明命令和操作流程 - 更新合约地址,确保与新部署的合约匹配 - 添加快速网络连接测试脚本,验证合约连接和网络状态 - 实现完整测试场景脚本,模拟用户操作和流动性管理
238 lines
8.1 KiB
Solidity
238 lines
8.1 KiB
Solidity
// 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));
|
|
}
|
|
} |