上一章我们验证了精确输出场景的价格计算,本篇回到 Pair 合约,针对长期被忽视的手续费缺口进行全面修复,确保自动做市恒定乘积模型不被破坏。 原始的 swap 函数虽通过恒定乘积检查限制输出,却没有对输入资金征收 0.3% swap 手续费,导致流动池实际增值低于预期,进而影响价格稳定性与协议收入
本系列延续前几篇的深度拆解,聚焦于 UniswapV2 核心合约的关键分支与微调逻辑,为读者提供可直接落地的实践指南。
上一章我们验证了精确输出场景的价格计算,本篇回到 Pair
合约,针对长期被忽视的手续费缺口进行全面修复,确保自动做市恒定乘积模型不被破坏。
原始的 swap
函数虽通过恒定乘积检查限制输出,却没有对输入资金征收 0.3% swap 手续费,导致流动池实际增值低于预期,进而影响价格稳定性与协议收入。为了避免后续功能建立在错误的基石之上,本篇优先完成手续费修复。
k
值不下降。if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
uint256 balance0 = IERC20(token0).balanceOf(address(this));
uint256 balance1 = IERC20(token1).balanceOf(address(this));
在 _safeTransfer
执行后立即读取余额,可获得“转出后、入账前”的即时状态,有助于推断用户实际的输入金额。
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();
把 reserve
视为“旧余额”,即可通过简单比较得到新增资金;当两个方向都为零时直接回滚,避免免费套利。
uint256 balance0Adjusted = (balance0 * 1000) - (amount0In * 3);
uint256 balance1Adjusted = (balance1 * 1000) - (amount1In * 3);
if (
balance0Adjusted * balance1Adjusted <
uint256(reserve0_) * uint256(reserve1_) * (1000**2)
) revert InvalidK();
通过将余额放大 1000 倍,再减去 amountIn * 3
,用整数模拟 0.3% 手续费;比较时同步放大旧储备,确保乘积守恒。在 Solidity 0.8 环境下,溢出自动被捕获,自定义错误 InvalidK
则提供清晰的失败原因。
(x + Δx) * (y - Δy) ≥ x * y
。Δx * (1 - 0.003)
,直接比较未扣费的余额会低估乘积。Pair
层级,调用方无需关心手续费细节,符合“单一职责 + 不重复自己”的设计准则。Pair
,外围模块无需重复处理,降低冗余。UniswapV2Library
提供 getAmountOut
等方法,测试与业务逻辑都应复用,避免数据泥团。scripts/test.sh
统一触发测试,日志会自动写入 logs/
目录。.env
与 foundry.toml
与项目模板一致,避免环境变量引起的精度差异。pragma solidity 0.8.30;
import {Test} from "forge-std/Test.sol";
import {UniswapV2Pair} from "../../src/core/UniswapV2Pair.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract PairFeeTest is Test {
ERC20Mock token0;
ERC20Mock token1;
UniswapV2Pair pair;
function setUp() public {
token0 = new ERC20Mock("TOKEN0", "TK0", address(this), 0);
token1 = new ERC20Mock("TOKEN1", "TK1", address(this), 0);
pair = new UniswapV2Pair();
pair.initialize(address(token0), address(token1));
token0.mint(address(this), 10 ether);
token1.mint(address(this), 20 ether);
token0.transfer(address(pair), 1 ether);
token1.transfer(address(pair), 2 ether);
pair.mint(address(this));
}
function testSwapRevertsWhenFeeUnpaid() public {
token0.transfer(address(pair), 0.1 ether);
vm.expectRevert(InvalidK.selector);
pair.swap(0, 0.181322178776029827 ether, address(this), "");
}
function testSwapSucceedsAfterFeeDeduction() public {
token0.transfer(address(pair), 0.1 ether);
pair.swap(0, 0.181322178776029826 ether, address(this), "");
(uint112 reserve0After, uint112 reserve1After,) = pair.getReserves();
assertGt(uint256(reserve0After) * uint256(reserve1After), 2 ether);
}
}
./scripts/test.sh
,触发 Foundry 测试用例。InvalidK
回滚与成功交易均按预期发生。UniswapV2Library
,避免出现冗余函数或数据泥团。手续费缺口看似不起眼,却会破坏 UniswapV2 最核心的价格发现机制。本篇逐行拆解 swap
函数,展示如何在不改变接口的前提下修复 bug、稳固架构,并利用 Foundry 测试验证修复。掌握这套思路后,你可以在更复杂的衍生需求中保持代码的透明、稳定与可维护。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!