4. 代币交换
任务目标
在自动化做市商(AMM)合约中实现代币交换功能:用户向合约支付输入代币,合约根据恒定乘积公式 x * y = k 计算应输出的代币数量,并在交换过程中收取 0.3% 的手续费。手续费保留在池内,使流动性提供者受益。
核心逻辑
交换的核心是恒定乘积公式。设池中代币A数量为 reserveA,代币B数量为 reserveB。用户支付 amountInA 个代币A,扣除 0.3% 手续费后,实际参与交换的数量为 amountInA * 0.997。根据公式:
(reserveA + amountInA * 0.997) * (reserveB - amountOutB) = reserveA * reserveB
解得输出数量:
amountOutB = reserveB - (reserveA * reserveB) / (reserveA + amountInA * 0.997)
合约需先完成代币转入,再计算并转出输出代币,最后更新储备量。
完整实现(Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
}
contract SimpleAMM {
IERC20 public tokenA;
IERC20 public tokenB;
uint256 public reserveA;
uint256 public reserveB;
uint256 public constant FEE_NUMERATOR = 997; // 0.3% 手续费
uint256 public constant FEE_DENOMINATOR = 1000;
constructor(address _tokenA, address _tokenB) {
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
// 首次注入流动性,建立初始储备
function addInitialLiquidity(uint256 amountA, uint256 amountB) external {
require(reserveA == 0 && reserveB == 0, "already initialized");
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
reserveA = amountA;
reserveB = amountB;
}
// 代币A交换代币B
function swapAForB(uint256 amountInA, uint256 minOutB) external returns (uint256 amountOutB) {
require(amountInA > 0, "input must be positive");
tokenA.transferFrom(msg.sender, address(this), amountInA);
uint256 amountInAfterFee = amountInA * FEE_NUMERATOR / FEE_DENOMINATOR;
uint256 newReserveA = reserveA + amountInAfterFee;
// 恒定乘积:x*y=k,输出后池内B数量
uint256 newReserveB = reserveA * reserveB / newReserveA;
amountOutB = reserveB - newReserveB;
require(amountOutB >= minOutB, "slippage too high");
require(amountOutB < reserveB, "insufficient output");
reserveA = newReserveA;
reserveB = newReserveB;
tokenB.transfer(msg.sender, amountOutB);
}
// 代币B交换代币A(对称实现)
function swapBForA(uint256 amountInB, uint256 minOutA) external returns (uint256 amountOutA) {
require(amountInB > 0, "input must be positive");
tokenB.transferFrom(msg.sender, address(this), amountInB);
uint256 amountInAfterFee = amountInB * FEE_NUMERATOR / FEE_DENOMINATOR;
uint256 newReserveB = reserveB + amountInAfterFee;
uint256 newReserveA = reserveA * reserveB / newReserveB;
amountOutA = reserveA - newReserveA;
require(amountOutA >= minOutA, "slippage too high");
require(amountOutA < reserveA, "insufficient output");
reserveA = newReserveA;
reserveB = newReserveB;
tokenA.transfer(msg.sender, amountOutA);
}
// 查询当前交换输出(不含滑点检查)
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256) {
uint256 amountInAfterFee = amountIn * FEE_NUMERATOR / FEE_DENOMINATOR;
uint256 newReserveIn = reserveIn + amountInAfterFee;
uint256 newReserveOut = reserveIn * reserveOut / newReserveIn;
return reserveOut - newReserveOut;
}
}
运行与验证
环境依赖:Remix IDE(Solidity 0.8.19+)或 Hardhat + ethers.js。需要两个测试ERC20代币合约(如标准OpenZeppelin ERC20)。
部署与测试步骤:
- 部署两个ERC20代币合约,记录地址。
- 部署
SimpleAMM,构造参数传入两个代币地址。 - 调用
addInitialLiquidity:先分别向AMM合约授权代币,然后注入流动性(例如各1000个代币)。 - 调用
swapAForB:先授权AMM花费代币A,然后传入输入数量(如100)和最小输出数量(如0)。
验证判定标准:
- 交换后调用
reserveA()和reserveB(),确认储备量已更新。 - 检查用户收到的代币B数量与
getAmountOut(100, reserveA, reserveB)返回值一致。 - 验证恒定乘积:交换后
reserveA * reserveB应大于交换前的乘积(因手续费留在池内)。 - 示例:初始储备各1000,输入100个A,手续费3个A,实际参与交换97个A。预期输出约为
1000 - (1000*1000)/(1000+97) ≈ 88.42个B(实际取整为88)。交换后reserveA = 1097,reserveB = 912,乘积1097 * 912 = 1,000,464,大于初始1,000,000,差额即手续费。
常见问题
- 精度问题:Solidity整数除法向下取整,实际输出比理论值略小,属正常现象。
- 滑点保护:调用时设置
minOutB为可接受的最小输出,防止交易被抢跑导致损失。 - 首次流动性:本合约仅允许一次性初始化储备,实际Uniswap V2使用铸造LP代币方式支持多次添加流动性。