本文是 UniswapV2 深入解析系列的第四篇文章,专注于去中心化交易所的核心功能——代币交换机制的实现。我们将深入探讨 UniswapV2 如何通过恒定乘积公式实现无许可的代币交换。
本文是 UniswapV2 深入解析系列的第四篇文章,专注于去中心化交易所的核心功能——代币交换机制的实现。我们将深入探讨 UniswapV2 如何通过恒定乘积公式实现无许可的代币交换。
通过本文,您将理解:
UniswapV2 代币交换的核心算法原理
恒定乘积公式在实际代码中的应用
如何设计安全的交换接口
去中心化代币交换是 UniswapV2 的核心功能,它允许用户在无需中介的情况下直接交换不同的 ERC20 代币。与传统中心化交易所不同,UniswapV2 使用自动做市商(AMM)模型,通过数学公式而非订单簿来确定交易价格。
在实现代币交换功能时,我们必须遵循以下核心原则:
UniswapV2 使用恒定乘积公式作为核心定价机制:
x * y = k
其中:
x
和 y
分别是两种代币的储备量k
是常数,代表流动性池的总价值/**
* @notice 代币交换函数
* @param amount0Out 期望获得的 token0 数量
* @param amount1Out 期望获得的 token1 数量
* @param to 接收代币的地址
* @param data 用于闪电贷的回调数据(本实现暂不支持)
* @dev 使用预转账模式,调用前需要先向合约转入要交换的代币
*/
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external {
// 至少需要指定一个输出数量
if (amount0Out <= 0 && amount1Out <= 0) revert InsufficientOutputAmount();
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // 获取储备金
if (amount0Out >= _reserve0 || amount1Out >= _reserve1) revert InsufficientLiquidity();
uint256 balance0;
uint256 balance1;
{
// 作用域限制,避免栈太深错误
address _token0 = token0;
address _token1 = token1;
if (to == _token0 || to == _token1) revert InvalidTo();
// 发送代币
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
// 闪电贷回调(暂不实现)
// if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
设计要点说明:
data
参数) uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
if (amount0In <= 0 && amount1In <= 0) revert InsufficientInputAmount();
核心机制解析:
amount_in = current_balance - (old_reserve - amount_out)
{
// 作用域限制,避免栈太深错误
// 验证 K 常数:扣除 0.3% 手续费后,K 值应该不减少
uint256 balance0Adjusted = (balance0 * 1000) - (amount0In * 3);
uint256 balance1Adjusted = (balance1 * 1000) - (amount1In * 3);
if (balance0Adjusted * balance1Adjusted < uint256(_reserve0) * _reserve1 * (1000**2))
revert InsufficientInputAmount();
}
数学原理详解:
balance_adjusted = balance * 1000 - amount_in * 3
(x' * 997) * (y' * 997) >= x * y * 997²
手续费扣除机制解析:
手续费率:0.3% = 3/1000
计算方式:
balance0Adjusted = balance0 * 1000 - amount0In * 3
balance1Adjusted = balance1 * 1000 - amount1In * 3
数学原理:
amount0In * 3 = 300
balance0 * 1000 - 300 = balance0 * 1000 - 100 * 3
验证逻辑:
balance0Adjusted * balance1Adjusted
uint256(_reserve0) * _reserve1 * (1000**2)
关键点:手续费并不是实际转走的,而是通过数学验证的方式"虚拟扣除",确保交易符合扣除手续费后的恒定乘积公式。这些手续费实际上留在了流动性池中,增加了流动性提供者的收益。
_update(balance0, balance1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
完整流程总结:
如果用户不先将代币转入合约,直接调用 swap
函数会发生什么?
输入数量计算为零:
uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
由于 balance0
和 balance1
等于储备量(没有新增代币),计算出的 amount0In
和 amount1In
都会是 0。
触发安全检查失败:
if (amount0In <= 0 && amount1In <= 0) revert InsufficientInputAmount();
由于两个输入数量都是 0,交易会立即回滚,抛出 InsufficientInputAmount
错误。
这种设计确保了:
首先创建测试合约:
// test/UniswapV2Pair.swap.t.sol
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/core/UniswapV2Pair.sol";
import "./mocks/MockERC20.sol";
contract UniswapV2PairSwapTest is Test {
UniswapV2Pair pair;
MockERC20 tokenA;
MockERC20 tokenB;
address user = makeAddr("user");
function setUp() public {
tokenA = new MockERC20("TokenA", "TKA", 18);
tokenB = new MockERC20("TokenB", "TKB", 18);
pair = new UniswapV2Pair();
pair.initialize(address(tokenA), address(tokenB));
// 添加初始流动性
tokenA.mint(address(this), 10000 ether);
tokenB.mint(address(this), 10000 ether);
tokenA.transfer(address(pair), 1000 ether);
tokenB.transfer(address(pair), 1000 ether);
pair.mint(address(this));
}
}
function testSwapToken0ForToken1() public {
// 准备交换:用 100 TokenA 换取 TokenB
uint256 amountIn = 100 ether;
tokenA.mint(user, amountIn);
vm.startPrank(user);
// 1. 先转入要交换的代币
tokenA.transfer(address(pair), amountIn);
// 2. 计算预期输出(考虑0.3%手续费)
uint256 expectedOut = getAmountOut(amountIn, 1000 ether, 1000 ether);
// 3. 执行交换
pair.swap(0, expectedOut, user);
// 4. 验证结果
assertEq(tokenB.balanceOf(user), expectedOut);
assertEq(tokenA.balanceOf(user), 0);
vm.stopPrank();
}
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
internal
pure
returns (uint256 amountOut)
{
uint256 amountInWithFee = amountIn * 997; // 扣除0.3%手续费
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
amountOut = numerator / denominator;
}
function testSwapWithBothOutputs() public {
uint256 amount0In = 100 ether;
uint256 amount1In = 50 ether;
tokenA.mint(user, amount0In);
tokenB.mint(user, amount1In);
vm.startPrank(user);
// 同时输入两种代币
tokenA.transfer(address(pair), amount0In);
tokenB.transfer(address(pair), amount1In);
// 指定两种输出数量
uint256 amount0Out = 20 ether;
uint256 amount1Out = 30 ether;
pair.swap(amount0Out, amount1Out, user);
// 验证恒定乘积
(uint112 reserve0, uint112 reserve1,) = pair.getReserves();
uint256 newProduct = uint256(reserve0) * uint256(reserve1);
uint256 oldProduct = 1000 ether * 1000 ether;
assertGe(newProduct, oldProduct);
vm.stopPrank();
}
function testSwapInsufficientLiquidity() public {
tokenA.mint(user, 100 ether);
vm.startPrank(user);
tokenA.transfer(address(pair), 100 ether);
// 尝试提取超过储备的代币量
vm.expectRevert(InsufficientLiquidity.selector);
pair.swap(0, 2000 ether, user); // 超过储备的 1000 ether
vm.stopPrank();
}
function testSwapInvalidK() public {
vm.startPrank(user);
// 不转入任何代币就尝试交换
vm.expectRevert(InvalidK.selector);
pair.swap(0, 100 ether, user);
vm.stopPrank();
}
在项目根目录执行以下命令:
# 运行交换相关测试
forge test --match-path test/UniswapV2Pair.swap.t.sol -v
# 运行详细测试,显示日志
forge test --match-test testSwapToken0ForToken1 -vvv
# 生成测试覆盖率报告
forge coverage --match-path test/UniswapV2Pair.swap.t.sol
_update
函数中一次性更新所有状态在实际部署时,需要考虑:
本文深入讲解了 UniswapV2 代币交换机制的核心实现,通过详细的代码解析和完整的测试示例,您应该已经掌握了:
恒定乘积公式在代码中的具体应用
如何设计安全而灵活的交换接口
使用 Foundry 框架进行完整的功能测试
在下一篇文章中,我们将实现交易手续费机制和流动性提供者奖励系统,进一步完善我们的 DEX 实现。
本文所有代码示例和完整实现都可以在项目仓库中找到,欢迎克隆代码进行实践学习:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!