继第 20 篇讨论手续费修复之后,本篇将围绕 Pair 合约,介绍闪电贷能力如何与手续费验证逻辑融合。阅读本篇后,你将理解闪电贷的业务场景、合约设计要点、实际实现方式以及 Foundry 测试策略。
继第 20 篇讨论手续费修复之后,本篇将围绕 Pair 合约,介绍闪电贷能力如何与手续费验证逻辑融合。阅读本篇后,你将理解闪电贷的业务场景、合约设计要点、实际实现方式以及 Foundry 测试策略。
闪电贷允许在同一笔交易中借入任意数量的资产,只要交易结束前连同费用一起归还。由于无需抵押,这一特性对套利、再融资和清算策略极具吸引力,同时也对合约安全提出更高要求。Uniswap V2 通过在 swap
流程中引入回调机制,实现对借贷行为的扩展,并复用恒定乘积校验来保证资金安全。
UniswapV2Pair
负责发放与回收闪电贷;借款方必须实现 IUniswapV2Callee
接口;UniswapV2Library
继续承担储备查询、金额推导等通用计算。bytes data
参数决定是否调用回调。一旦调用,回调合约需在逻辑结束前将借出资产与手续费转回 Pair。k = reserve0 * reserve1
校验快照,自动确保手续费到位;任何违背手续费规则的归还都会导致交易回滚。下方展示的是 UniswapV2Pair
中 swap
函数的关键片段。通过新增 data
参数与回调调用,即可在不破坏原有接口行为的情况下支持闪电贷。
/// @title UniswapV2Pair 掉期执行
/// @notice 支持输出代币与闪电贷回调,在同一交易内完成清算
/// @param amount0Out 需要转出的 token0 数量
/// @param amount1Out 需要转出的 token1 数量
/// @param to 接收代币或回调的目标合约地址
/// @param data 闪电贷约定的附加参数,非空时触发回调
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) public {
// 1. 参数校验 & 储备更新(逻辑与前文保持一致)
// 2. 乐观转账:按用户请求先行发送资产
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
// 3. 闪电贷回调:仅在 data 非空时触发
if (data.length > 0) {
IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
}
// 4. 更新储备并执行恒定乘积校验,确保手续费被动结算
_update(balance0, balance1, _reserve0, _reserve1);
require(_k(balance0, balance1) >= _k(_reserve0, _reserve1), "UniswapV2: K");
}
借款方需遵循统一接口,避免循环依赖或重复实现。
/// @title UniswapV2 闪电贷回调接口
/// @notice Pair 在完成乐观转账后回调此接口
interface IUniswapV2Callee {
/// @param sender 触发 swap 的地址(通常为 Router)
/// @param amount0 借出的 token0 数量
/// @param amount1 借出的 token1 数量
/// @param data 借款方自定义的执行参数
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
}
借款合约在回调函数内执行业务逻辑,并负责将本金与手续费转回 Pair,必要时可调用 Router 进行链上套利或组合操作。
k
将下降,交易直接回退,从而强制补齐手续费。amount * 1000 / 997 - amount + 1
公式预存手续费,其中加一是为应对整型向下取整造成的进位风险。以下为示例借款合约,实现回调并归还费用。
contract Flashloaner is IUniswapV2Callee {
IERC20 public immutable token;
IUniswapV2Pair public immutable pair;
constructor(address token_, address pair_) {
token = IERC20(token_);
pair = IUniswapV2Pair(pair_);
}
/// @notice 发起闪电贷,并在回调内完成偿还
/// @param amount 期望借入的代币数量
/// @param params 业务逻辑所需的附加参数
function executeFlashloan(uint256 amount, bytes calldata params) external {
pair.swap(0, amount, address(this), params);
}
/// @inheritdoc IUniswapV2Callee
function uniswapV2Call(
address,
uint256,
uint256 amount1,
bytes calldata params
) external override {
require(msg.sender == address(pair), "Flashloaner: only pair");
// 在此处执行套利、清算等业务逻辑
bytes32 strategy = abi.decode(params, (bytes32));
_runStrategy(strategy, amount1);
uint256 fee = (amount1 * 1000) / 997 - amount1 + 1;
token.transfer(address(pair), amount1 + fee);
}
function _runStrategy(bytes32 strategy, uint256 amount) internal {
// 预留业务扩展点,示例中不做实际操作
}
}
Foundry 测试通过部署 Flashloaner
并验证手续费归还情况,完整示例如下:
contract UniswapV2PairFlashloanTest is Test {
using SafeERC20 for IERC20;
UniswapV2Pair internal pair;
UniswapV2Factory internal factory;
IERC20 internal token0;
IERC20 internal token1;
function setUp() public {
// 初始化工厂、代币、交易对并注入初始储备
}
/// @notice 闪电贷会收取 0.3% 掉期手续费
function test_FlashloanChargesSwapFee() public {
Flashloaner fl = new Flashloaner(address(token1), address(pair));
uint256 amount = 0.1 ether;
uint256 fee = (amount * 1000) / 997 - amount + 1;
deal(address(token1), address(fl), fee);
(uint256 reserve0Before, uint256 reserve1Before) =
UniswapV2Library.getReserves(address(factory), address(token0), address(token1));
fl.executeFlashloan(amount, abi.encode(bytes32("ARBITRAGE")));
(uint256 reserve0After, uint256 reserve1After) =
UniswapV2Library.getReserves(address(factory), address(token0), address(token1));
assertEq(reserve0After, reserve0Before, "闪电贷不应改变另一侧储备");
assertEq(reserve1After, reserve1Before + fee, "手续费应累计进储备");
assertEq(token1.balanceOf(address(fl)), 0, "借款方应清空余额");
}
}
./scripts/test.sh
运行 Foundry 用例,确保日志输出保存于 logs/
目录。UniswapV2Library.getReserves
与 quote
,避免重复计算。bytes data
,确保外围协议能够扩展参数格式。https://github.com/RyanWeb31110/uniswapv2_tech
欢迎读者克隆仓库,配合本系列文章进行实战练习。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!