前言:闪电贷在 DeFi 中的核心作用

在去中心化金融(DeFi)领域,闪电贷(Flash Loan)代表了区块链技术的一项革命性创新。它允许用户在无需提供抵押的情况下即时借入巨额资金,前提是必须在同一交易内完成偿还。这种机制充分利用了以太坊交易的原子性——即交易要么全部执行成功,要么全部回滚——从而为套利、清算和资本优化等复杂策略提供了安全高效的执行环境。本文将对基于 Uniswap V2 的闪电贷套利合约进行函数级别的细致剖析,结合测试案例和执行流程,揭示其背后的技术原理与实际应用价值。

Uniswap V2 作为恒定乘积自动做市商(AMM)的典范,其闪电兑换(Flash Swap)机制进一步扩展了闪电贷的应用边界。与传统闪电贷(如 Aave)要求偿还相同资产不同,Uniswap V2 的闪电兑换允许用户以等值其他资产偿付,这为跨池套利提供了更大的灵活性。通过本文的分析,您将深入理解这一机制如何在代码层面实现高效的资金循环。

一、合约接口与整体架构

1.1 Uniswap V2 接口定义

合约首先定义了三个核心接口,这些是与 Uniswap V2 协议交互的基础:

  • IUniswapV2Pair:处理代币对的核心操作,包括 token0()token1()swap()getReserves()。这些方法确保了合约能精确查询储备并执行交换。
  • IUniswapV2Factory:通过 getPair() 方法获取特定代币对的地址,实现动态池子定位。
  • IUniswapV2Router02:提供 getAmountOut()getAmountIn() 等纯函数,用于计算交换量,避免手动实现 AMM 公式可能引入的精度误差。

这些接口的设计体现了模块化和标准化原则,确保合约与 Uniswap V2 的无缝集成,同时便于未来扩展。

1.2 状态变量与事件机制

合约的状态变量设计简洁高效:

  • uniswapV2FactoryuniswapV2Router:存储协议地址,支持测试时的动态配置。
  • owner:合约所有者,用于访问控制。
  • FlashSwapExecuted 事件:记录套利执行细节,包括池子地址、代币类型、借贷量和利润,便于链上监控和审计。

这种精炼的状态管理降低了存储成本,并提升了合约的安全性。

二、函数级代码剖析

2.1 构造函数 (constructor)

1constructor() {
2    uniswapV2Factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
3    uniswapV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
4    owner = msg.sender;
5}

构造函数初始化了 Uniswap V2 的主网地址,并设置所有者。这种硬编码方式确保了部署时的即用性,同时为生产环境提供了稳定性。

2.2 访问控制修饰符 (onlyOwner)

1modifier onlyOwner() {
2    require(msg.sender == owner, 'Not owner');
3    _;
4}

这一修饰符构成了合约的安全基石,确保敏感操作(如套利执行和地址配置)仅限于所有者执行,防范潜在的外部滥用。

2.3 地址配置函数 (setUniswapAddresses)

1function setUniswapAddresses(address factory, address router) external onlyOwner {
2    uniswapV2Factory = factory;
3    uniswapV2Router = router;
4}

此函数增强了合约的适应性,允许在测试或分叉环境中切换地址,体现了开发中的最佳实践:生产与测试分离。

2.4 套利启动函数 (executeFlashSwap)

 1function executeFlashSwap(
 2    address poolA, // 价格较低的池子
 3    address poolB, // 价格较高的池子
 4    address tokenA, // 要借贷的代币
 5    address tokenB, // 要交换的代币
 6    uint256 amountToBorrow // 借贷数量
 7) external onlyOwner {
 8    // 验证池子地址
 9    require(poolA != address(0) && poolB != address(0), 'Invalid pool addresses');
10
11    // 从 poolA 开始闪电贷
12    IUniswapV2Pair pair = IUniswapV2Pair(poolA);
13    address token0 = pair.token0();
14    address token1 = pair.token1();
15
16    uint256 amount0Out = tokenA == token0 ? amountToBorrow : 0;
17    uint256 amount1Out = tokenA == token1 ? amountToBorrow : 0;
18
19    // 编码数据传递给回调函数
20    bytes memory data = abi.encode(poolB, tokenA, tokenB, amountToBorrow);
21
22    // 执行闪电贷
23    pair.swap(amount0Out, amount1Out, address(this), data);
24}

作为套利的入口,此函数验证输入、编码回调数据,并触发 swap() 以启动闪电兑换。关键在于数据编码,确保回调函数能访问必要参数。

2.5 回调核心函数 (uniswapV2Call)

 1function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {
 2    // 验证调用者是合法的 Uniswap V2 配对合约
 3    address token0 = IUniswapV2Pair(msg.sender).token0();
 4    address token1 = IUniswapV2Pair(msg.sender).token1();
 5    address pair = IUniswapV2Factory(uniswapV2Factory).getPair(token0, token1);
 6    require(msg.sender == pair, 'Invalid pair');
 7    require(sender == address(this), 'Invalid sender');
 8
 9    // 解码数据
10    (address poolB, address _tokenA, address _tokenB, uint256 amountBorrowed) = abi.decode(
11      data,
12      (address, address, address, uint256)
13    );
14
15    // 获取借到的代币数量
16    console.log(">>>>>>>>>>>>>>>>> Borrowed tokenA", amountBorrowed);
17    uint256 amountReceived = amount0 > 0 ? amount0 : amount1;
18    console.log(">>>>>>>>>>>>>>>>> Expected TokenA:", amountReceived);
19    console.log(">>>>>>>>>>>>>>>>> Received TokenA:", IERC20(_tokenA).balanceOf(address(this)));
20    
21    // 在 poolB 中将 tokenA 兑换为 tokenB
22    console.log(">>>>>>>>>>>>>>>>> In poolB, exchange tokenA to tokenB");
23    _swapOnPool(poolB, _tokenA, _tokenB, amountReceived);
24    console.log(">>>>>>>>>>>>>>>>> Received TokenB:", IERC20(_tokenB).balanceOf(address(this)));
25
26    // 计算需要还款的数量(包含手续费)
27    uint256 amountToRepay = _calculateRepayAmount(amountBorrowed);
28    console.log(">>>>>>>>>>>>>>>>> Need repay TokenA:", amountToRepay);
29
30    // 从amountOut中拿出一部分用于还款
31    // 这里我们计算需要多少_tokenB来偿还所需的_tokenA(计算输出需要偿还的amountToRepay个A,需要输入多少个B)
32    uint256 amountToSwapBack = _calculateAmountToSwapBack(msg.sender, _tokenB, amountToRepay);
33    console.log(">>>>>>>>>>>>>>>>> Need repay TokenB for TokenA in poolA:", amountToSwapBack);
34
35    // 检查我们是否有足够多的_tokenB用于偿还_tokenA
36    uint256 balanceOfTokenB = IERC20(_tokenB).balanceOf(address(this));
37    require(balanceOfTokenB >= amountToSwapBack, 'Insufficient tokenB for repayment');
38    console.log(">>>>>>>>>>>>>>>>> balanceOfTokenB:", IERC20(_tokenB).balanceOf(address(this)));
39
40    // 转移_tokenB给配对合约(msg.sender 是 poolA)
41    IERC20(_tokenB).transfer(msg.sender, amountToSwapBack);
42
43    // 现在我们已经把需要的_tokenB发送给配对合约,配对合约会将其与它持有的_tokenA进行交换
44    // 这里不再直接调用swap,而是通过配对合约在swap结束时自动完成交换
45    // 配对合约会验证我们是否返回了足够的_tokenA
46
47    // 由于我们已经发送了正确的amountToSwapBack到配对合约A
48    // 并且我们计算了所需的还款金额,所以合约能验证通过
49
50    // 计算利润
51    uint256 remainingTokenB = IERC20(_tokenB).balanceOf(address(this));
52    console.log(">>>>>>>>>>>>>>>>> Remaining TokenB:", remainingTokenB);
53
54    // 将剩余代币转给 owner
55    if (remainingTokenB > 0) {
56      IERC20(_tokenB).transfer(owner, remainingTokenB);
57    }
58
59    emit FlashSwapExecuted(msg.sender, poolB, _tokenA, _tokenB, amountBorrowed, remainingTokenB);
60  }

此函数是闪电兑换的核心引擎:验证调用者、解码数据、执行套利交换、计算并偿还借款,最后转移利润。日志输出便于调试,强调了资金流动的透明性。

2.6 内部交换函数 (_swapOnPool)

 1function _swapOnPool(address pool, address tokenIn, address tokenOut, uint256 amountIn) internal {
 2    IUniswapV2Pair pair = IUniswapV2Pair(pool); // poolB
 3    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
 4
 5    address _token0 = pair.token0();
 6
 7    bool token0IsTokenIn = tokenIn == _token0;
 8
 9    (uint112 reserveIn, uint112 reserveOut) = token0IsTokenIn ? (reserve0, reserve1) : (reserve1, reserve0);
10
11    // 计算输出数量
12    uint256 amountOut = IUniswapV2Router02(uniswapV2Router).getAmountOut(amountIn, reserveIn, reserveOut);
13    console.log(">>>>>>>>>>>>>>>>> Expected TokenB:", amountOut);
14
15    // 转移代币到配对合约
16    IERC20(tokenIn).transfer(pool, amountIn);
17
18    // 执行交换 - 根据tokenIn是token0还是token1来确定输出方向
19    uint256 amount0Out = tokenOut == _token0 ? amountOut : 0;
20    uint256 amount1Out = tokenOut == _token0 ? 0 : amountOut;
21
22    pair.swap(amount0Out, amount1Out, address(this), new bytes(0));
23}

函数处理套利中的关键交换步骤,利用路由计算输出并执行 swap(),确保滑点最小化。

2.7 还款计算函数 (_calculateRepayAmount)

1function _calculateRepayAmount(uint256 amountBorrowed) internal pure returns (uint256) {
2    // Uniswap V2 手续费是 0.3%
3    return (amountBorrowed * 1000) / 997 + 1;
4}

此纯函数精确计算 Uniswap V2 的 0.3% 手续费溢价,确保偿还符合 AMM 的 K 值恒定要求。

2.8 反向交换计算 (_calculateAmountToSwapBack)

 1function _calculateAmountToSwapBack(
 2    address pool,
 3    address tokenIn,
 4    uint256 amountOutNeeded
 5) internal view returns (uint256 amountIn) {
 6    IUniswapV2Pair pair = IUniswapV2Pair(pool); // poolA
 7    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
 8
 9    address _token0 = pair.token0();
10    bool token0IsTokenIn = tokenIn == _token0;
11
12    (uint112 reserveIn, uint112 reserveOut) = token0IsTokenIn ? (reserve0, reserve1) : (reserve1, reserve0);
13
14    amountIn = IUniswapV2Router02(uniswapV2Router).getAmountIn(amountOutNeeded, reserveIn, reserveOut);
15}

利用 getAmountIn() 计算偿还所需输入量,避免手动公式计算的潜在误差。

2.9 紧急撤资函数 (emergencyWithdraw)

1function emergencyWithdraw(address token) external onlyOwner {
2    uint256 balance = IERC20(token).balanceOf(address(this));
3    if (balance > 0) {
4        IERC20(token).transfer(owner, balance);
5    }
6}

作为后备机制,此函数允许所有者提取资金,增强合约的鲁棒性。

三、测试环境与函数分析

3.1 Mock 模拟合约

测试使用 Mock 版本模拟 ERC20、Pair、Factory 和 Router,确保隔离环境下的可控性。这些模拟实现了核心逻辑,如 K 值验证(balance0Adjusted * balance1Adjusted >= _reserve0 * _reserve1 * 1000**2),准确复现 Uniswap V2 的行为。

3.2 初始化函数 (setUp)

 1function setUp() public {
 2    // 部署代币
 3    tokenA = new MockERC20("Token A", "TKA");
 4    tokenB = new MockERC20("Token B", "TKB");
 5
 6    // 部署工厂和路由器
 7    factory = new MockUniswapV2Factory();
 8    router = new MockUniswapV2Router();
 9
10    // 部署配对合约
11    poolA = new MockUniswapV2Pair(address(tokenA), address(tokenB));
12    poolB = new MockUniswapV2Pair(address(tokenA), address(tokenB));
13
14    // 设置工厂中的配对信息
15    factory.setPair(address(tokenA), address(tokenB), address(poolA));
16
17    // 设置初始储备 (pairA: 1A = 1000B, 即 1:1000 的比例)
18    // 设置为 1000000 A 和 1000000000 B (比例为 1:1000)
19    tokenA.mint(address(poolA), 1000000);
20    tokenB.mint(address(poolA), 1000000000);
21    poolA.setReserves(1000000, 1000000000);
22
23    // 设置pairB的储备 (pairB: 1A = 1300B, 即 1:1300 的比例)
24    // 设置为 1000000 A 和 1300000000 B (比例为 1:1300)
25    tokenA.mint(address(poolB), 1000000);
26    tokenB.mint(address(poolB), 1300000000);
27    poolB.setReserves(1000000, 1300000000);
28
29    // 部署FlashSwapTestable合约
30    flashSwap = new FlashSwap();
31
32    // 设置模拟的工厂和路由器地址
33    flashSwap.setUniswapAddresses(address(factory), address(router));
34
35    // 将FlashSwapTestable合约的owner设置为测试合约(当前this)
36    // flashSwap合约的当前owner是部署合约时的msg.sender(也就是FlashSwapTest合约)
37    // 因此owner变量应该设置为FlashSwapTest合约的地址
38    owner = address(this);
39}

初始化创建价差场景(PoolA: 1A=1000B;PoolB: 1A=1300B),模拟真实套利机会。

3.3 套利测试函数 (testFlashSwapArbitrage)

 1function testFlashSwapArbitrage() public {
 2        // 测试用例:验证FlashSwap套利交易逻辑
 3        // 场景:pairA中1A=1000B, pairB中1A=1300B
 4        // 从pairA借出来100个A,在pairB中用100个A兑换B代币,再在pairA中将B换成A还回去
 5
 6        // 记录初始状态
 7        uint256 initialTokenABalance = tokenA.balanceOf(owner);
 8        uint256 initialTokenBBalance = tokenB.balanceOf(owner);
 9
10        console.log("Initial A Balance:", initialTokenABalance);
11        console.log("Initial B Balance:", initialTokenBBalance);
12
13        // 调用FlashSwap合约进行套利交易
14        // 从poolA借A代币,在poolB中进行套利
15        flashSwap.executeFlashSwap(
16            address(poolA),     // 从poolA借A代币
17            address(poolB),     // 在poolB中进行套利
18            address(tokenA),    // 借贷的代币是A
19            address(tokenB),    // 交换的目标代币是B
20            100                 // 借贷数量是100
21        );
22
23        // 检查是否获利
24        uint256 finalTokenABalance = tokenA.balanceOf(owner);
25        uint256 finalTokenBBalance = tokenB.balanceOf(owner);
26
27        console.log("Final A Balance:", finalTokenABalance);
28        console.log("Final B Balance:", finalTokenBBalance);
29        console.log("A Balance Change:", int(finalTokenABalance) - int(initialTokenABalance));
30        console.log("B Balance Change:", int(finalTokenBBalance) - int(initialTokenBBalance));
31
32        // 验证最终的B代币余额有所增加(套利获利)
33        // 初始B代币余额应该加上最终收益等于最终B代币余额
34        assertGt(finalTokenBBalance, initialTokenBBalance);
35
36        // 输出日志显示获利结果
37        console.log("B Token Profit:", finalTokenBBalance - initialTokenBBalance);
38    }

测试验证了从借贷到利润转移的全链路,确保逻辑无误。

四、执行流程与结果剖析

为直观展示套利流程,以下是基于测试案例的时序图,描绘了关键交互:

sequenceDiagram
    participant Owner as Owner
    participant FlashSwap as FlashSwap Contract
    participant PoolA as PoolA (borrowing pool)
    participant PoolB as PoolB (arbitrage pool)
    participant TokenA as TokenA
    participant TokenB as TokenB
    participant Router as Uniswap Router

    Note over Owner,TokenB: 设置阶段:根据合约初始化和测试用例
    Note over Owner,TokenB: poolA: 1A = 1000B (1,000,000 A : 1,000,000,000 B)
    Note over Owner,TokenB: poolB: 1A = 1300B (1,000,000 A : 1,300,000,000 B)
    Note over Owner,TokenB: 借贷: 100 TokenA
    Owner->>FlashSwap: executeFlashSwap(poolA, poolB, TokenA, TokenB, 100)
    activate FlashSwap
    FlashSwap->>PoolA: swap(amount0Out=100, amount1Out=0, address(this), data)
    activate PoolA
    Note over PoolA: 在swap函数中触发uniswapV2Call回调
    PoolA->>FlashSwap: uniswapV2Call(this, 100, 0, data)
    activate FlashSwap
    Note over FlashSwap: 1. 验证调用者
    Note over FlashSwap: 2. 计算借贷数量: received 100 TokenA
    Note over FlashSwap: 3. 查询当前TokenA余额: IERC20(_tokenA).balanceOf
    Note over FlashSwap: 预期收到: 100 TokenA, 实际借到: 100 TokenA

    FlashSwap->>PoolB: _swapOnPool() - exchange 100 TokenA to TokenB
    Note over FlashSwap: 调用_swapOnPool(poolB, TokenA, TokenB, 100)
    activate PoolB
    PoolB->>TokenA: transfer(poolB, 100 TokenA)
    TokenA->>PoolB: transfer completed
    PoolB->>FlashSwap: swap(0, 129597, address(this), new bytes(0))
    FlashSwap->>PoolB: receive TokenB from swap (129597 TokenB)
    deactivate PoolB

    Note over FlashSwap: 4. 在poolB中完成交换: 100 TokenA -> 129597 TokenB
    Note over FlashSwap: 根据合约池B比例 1:1300, 100 A 应兑换 129597 TokenB
    Note over FlashSwap: amountOut = 100 * 997 / (1000000 * 1000 + 100 * 997) * 1300000000 ≈ 129597

    Note over FlashSwap: 5. 计算还款金额: 100 * 1000 / 997 + 1 = 101 TokenA 
    Note over FlashSwap: 6. 计算在poolA中需要交换多少TokenB来还TokenA
    Note over FlashSwap: amountIn = 1000 * 1000000000 * 101 / (997 * (1000000 - 101)) + 1 ≈ 101315
    FlashSwap->>Router: _calculateAmountToSwapBack(poolA, TokenB, 101)
    Router->>FlashSwap: return 101315 TokenB required

    Note over FlashSwap: 7. 验证TokenB余额: balanceOf >= 101315 TokenB 
    Note over FlashSwap: 8. 需要还款101 TokenA,需用101315 TokenB交换(101315来自_tokenB)
    FlashSwap->>PoolA: transfer(101315 TokenB to pairA contract)
    deactivate FlashSwap

    Note over PoolA: 9. 配对合约A自动计算并验证是否返回足够TokenA
    Note over PoolA: 根据公式 balanceAAdjusted * balanceBAdjusted >= _reserveA * _reserveB
    Note over PoolA: 借出100 TokenA,收到101315 TokenB,计算是否返还足额TokenA
    Note over PoolA: balanceAAdjusted = balanceA - amountAIn * 0.003 = 999900 - 0 * 0.003 = 999900
    Note over PoolA: balanceBAdjusted = balanceB - amountBIn * 0.003 = 1000101315 - 101315 * 0.003 ≈ 1000101011
    Note over PoolA: _reserveA = 1000000
    Note over PoolA: _reserveB = 1000000000
    Note over PoolA: 如果计算结果 999900 * 1000101011 ≥ 1000000 * 1000000000,则验证通过,交易完成
    PoolA-->>FlashSwap: Verify repayment and complete transaction

    Note over FlashSwap: Profit calculation:
    Note over FlashSwap: 总TokenB获得: 129597, 用于还款: 101315, 剩余: 28282 TokenB
    FlashSwap->>Owner: transfer(remaining 28282 TokenB to owner)
    deactivate PoolA
    Note over Owner,FlashSwap: FlashSwap套利完成,获得28282 TokenB利润

测试执行输出如下,展示了资金流动的精确轨迹:

 1$ forge test --match-test testFlashSwapArbitrage -vvv
 2[] Compiling...
 3[] Compiling 1 files with Solc 0.8.30
 4[] Solc 0.8.30 finished in 4.49s
 5Compiler run successful!
 6
 7Ran 1 test for test/FlashSwap.t.sol:FlashSwapTest
 8[PASS] testFlashSwapArbitrage() (gas: 176830)
 9Logs:
10  Initial A Balance: 0
11  Initial B Balance: 0
12  >>>>>>>>>>>>>>>>> Borrowed tokenA 100
13  >>>>>>>>>>>>>>>>> Expected TokenA: 100
14  >>>>>>>>>>>>>>>>> Received TokenA: 100
15  >>>>>>>>>>>>>>>>> In poolB, exchange tokenA to tokenB
16  >>>>>>>>>>>>>>>>> Expected TokenB: 129597
17  >>>>>>>>>>>>>>>>> amount0In: 100
18  >>>>>>>>>>>>>>>>> amount1In: 0
19  >>>>>>>>>>>>>>>>> balance0: 1000100
20  >>>>>>>>>>>>>>>>> balance1: 1299870403
21  >>>>>>>>>>>>>>>>> _reserve0: 1000000
22  >>>>>>>>>>>>>>>>> _reserve1: 1300000000
23  >>>>>>>>>>>>>>>>> Received TokenB: 129597
24  >>>>>>>>>>>>>>>>> Need repay TokenA: 101
25  >>>>>>>>>>>>>>>>> Need repay TokenB for TokenA in poolA: 101315
26  >>>>>>>>>>>>>>>>> balanceOfTokenB: 129597
27  >>>>>>>>>>>>>>>>> Remaining TokenB: 28282
28  >>>>>>>>>>>>>>>>> amount0In: 0
29  >>>>>>>>>>>>>>>>> amount1In: 101315
30  >>>>>>>>>>>>>>>>> balance0: 999900
31  >>>>>>>>>>>>>>>>> balance1: 1000101315
32  >>>>>>>>>>>>>>>>> _reserve0: 1000000
33  >>>>>>>>>>>>>>>>> _reserve1: 1000000000
34  Final A Balance: 0
35  Final B Balance: 28282
36  A Balance Change: 0
37  B Balance Change: 28282
38  B Token Profit: 28282

从输出可见,借入 100 TokenA,在 PoolB 兑换为 129,597 TokenB;偿还需 101 TokenA(含费),对应 101,315 TokenB;最终利润 28,282 TokenB。价差 30% 被手续费和滑点压缩至约 28%,体现了 AMM 的动态定价机制。

五、安全性与风险评估

尽管强大,闪电贷合约需警惕风险:

  • 价格操纵:攻击者可能通过大额交易扭曲池子价格。
  • 重入攻击:回调机制需严格验证调用者。
  • Gas 消耗:复杂逻辑可能导致交易失败或 MEV 抢跑。
  • 流动性风险:池子深度不足可能放大滑点。

推荐集成 Chainlink 预言机作为价格校验,并进行全面审计。

六、结语与应用展望

闪电贷不仅是技术创新,更是 DeFi 效率的体现。通过本文的剖析,我们见证了 Uniswap V2 如何通过原子交易实现无风险套利。这一机制在实际中可扩展至三角套利、多链桥接等领域,推动 DeFi 的进一步成熟。开发者应注重安全与优化,以最大化其潜力。

附录:完整合约代码

@contracts/src/FlashSwap.sol:

  1// SPDX-License-Identifier: MIT
  2pragma solidity ^0.8.13;
  3
  4import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
  5import "forge-std/console.sol";
  6
  7interface IUniswapV2Pair {
  8  function token0() external view returns (address);
  9
 10  function token1() external view returns (address);
 11
 12  function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
 13
 14  function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
 15}
 16
 17interface IUniswapV2Factory {
 18  function getPair(address tokenA, address tokenB) external view returns (address pair);
 19}
 20
 21interface IUniswapV2Router02 {
 22  function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
 23
 24  function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn);
 25}
 26
 27// 测试版本的FlashSwap合约,允许设置工厂和路由器地址
 28contract FlashSwap {
 29  address public uniswapV2Factory;
 30  address public uniswapV2Router;
 31
 32  address public owner;
 33
 34  event FlashSwapExecuted(
 35    address indexed poolA,
 36    address indexed poolB,
 37    address tokenA,
 38    address tokenB,
 39    uint256 amountBorrowed,
 40    uint256 profit
 41  );
 42
 43  modifier onlyOwner() {
 44    require(msg.sender == owner, 'Not owner');
 45    _;
 46  }
 47
 48  constructor() {
 49    uniswapV2Factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
 50    uniswapV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
 51    owner = msg.sender;
 52  }
 53
 54  // 允许测试时设置工厂和路由器地址
 55  function setUniswapAddresses(address factory, address router) external onlyOwner {
 56    uniswapV2Factory = factory;
 57    uniswapV2Router = router;
 58  }
 59
 60  // 执行闪电兑换套利
 61  function executeFlashSwap(
 62    address poolA, // 价格较低的池子
 63    address poolB, // 价格较高的池子
 64    address tokenA, // 要借贷的代币
 65    address tokenB, // 要交换的代币
 66    uint256 amountToBorrow // 借贷数量
 67  ) external onlyOwner {
 68    // 验证池子地址
 69    require(poolA != address(0) && poolB != address(0), 'Invalid pool addresses');
 70
 71    // 从 poolA 开始闪电贷
 72    IUniswapV2Pair pair = IUniswapV2Pair(poolA);
 73    address token0 = pair.token0();
 74    address token1 = pair.token1();
 75
 76    uint256 amount0Out = tokenA == token0 ? amountToBorrow : 0;
 77    uint256 amount1Out = tokenA == token1 ? amountToBorrow : 0;
 78
 79    // 编码数据传递给回调函数
 80    bytes memory data = abi.encode(poolB, tokenA, tokenB, amountToBorrow);
 81
 82    // 执行闪电贷
 83    pair.swap(amount0Out, amount1Out, address(this), data);
 84  }
 85
 86  // Uniswap V2 回调函数
 87  function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {
 88    // 验证调用者是合法的 Uniswap V2 配对合约
 89    address token0 = IUniswapV2Pair(msg.sender).token0();
 90    address token1 = IUniswapV2Pair(msg.sender).token1();
 91    address pair = IUniswapV2Factory(uniswapV2Factory).getPair(token0, token1);
 92    require(msg.sender == pair, 'Invalid pair');
 93    require(sender == address(this), 'Invalid sender');
 94
 95    // 解码数据
 96    (address poolB, address _tokenA, address _tokenB, uint256 amountBorrowed) = abi.decode(
 97      data,
 98      (address, address, address, uint256)
 99    );
100
101    // 获取借到的代币数量
102    console.log(">>>>>>>>>>>>>>>>> Borrowed tokenA", amountBorrowed);
103    uint256 amountReceived = amount0 > 0 ? amount0 : amount1;
104    console.log(">>>>>>>>>>>>>>>>> Expected TokenA:", amountReceived);
105    console.log(">>>>>>>>>>>>>>>>> Received TokenA:", IERC20(_tokenA).balanceOf(address(this)));
106    
107
108    // 在 poolB 中将 tokenA 兑换为 tokenB
109    console.log(">>>>>>>>>>>>>>>>> In poolB, exchange tokenA to tokenB");
110    _swapOnPool(poolB, _tokenA, _tokenB, amountReceived);
111    console.log(">>>>>>>>>>>>>>>>> Received TokenB:", IERC20(_tokenB).balanceOf(address(this)));
112
113    // 计算需要还款的数量(包含手续费)
114    uint256 amountToRepay = _calculateRepayAmount(amountBorrowed);
115    console.log(">>>>>>>>>>>>>>>>> Need repay TokenA:", amountToRepay);
116
117    // 从amountOut中拿出一部分用于还款
118    // 这里我们计算需要多少_tokenB来偿还所需的_tokenA(计算输出需要偿还的amountToRepay个A,需要输入多少个B)
119    uint256 amountToSwapBack = _calculateAmountToSwapBack(msg.sender, _tokenB, amountToRepay);
120    console.log(">>>>>>>>>>>>>>>>> Need repay TokenB for TokenA in poolA:", amountToSwapBack);
121
122    // 检查我们是否有足够多的_tokenB用于偿还_tokenA
123    uint256 balanceOfTokenB = IERC20(_tokenB).balanceOf(address(this));
124    require(balanceOfTokenB >= amountToSwapBack, 'Insufficient tokenB for repayment');
125    console.log(">>>>>>>>>>>>>>>>> balanceOfTokenB:", IERC20(_tokenB).balanceOf(address(this)));
126
127    // 转移_tokenB给配对合约(msg.sender 是 poolA)
128    IERC20(_tokenB).transfer(msg.sender, amountToSwapBack);
129
130    // 现在我们已经把需要的_tokenB发送给配对合约,配对合约会将其与它持有的_tokenA进行交换
131    // 这里不再直接调用swap,而是通过配对合约在swap结束时自动完成交换
132    // 配对合约会验证我们是否返回了足够的_tokenA
133
134    // 由于我们已经发送了正确的amountToSwapBack到配对合约A
135    // 并且我们计算了所需的还款金额,所以合约能验证通过
136
137    // 计算利润
138    uint256 remainingTokenB = IERC20(_tokenB).balanceOf(address(this));
139    console.log(">>>>>>>>>>>>>>>>> Remaining TokenB:", remainingTokenB);
140
141    // 将剩余代币转给 owner
142    if (remainingTokenB > 0) {
143      IERC20(_tokenB).transfer(owner, remainingTokenB);
144    }
145
146    emit FlashSwapExecuted(msg.sender, poolB, _tokenA, _tokenB, amountBorrowed, remainingTokenB);
147  }
148
149  // 执行交换
150  function _swapOnPool(address pool, address tokenIn, address tokenOut, uint256 amountIn) internal {
151    IUniswapV2Pair pair = IUniswapV2Pair(pool); // poolB
152    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
153
154    address _token0 = pair.token0();
155
156    bool token0IsTokenIn = tokenIn == _token0;
157
158    (uint112 reserveIn, uint112 reserveOut) = token0IsTokenIn ? (reserve0, reserve1) : (reserve1, reserve0);
159
160    // 计算输出数量
161    uint256 amountOut = IUniswapV2Router02(uniswapV2Router).getAmountOut(amountIn, reserveIn, reserveOut);
162    console.log(">>>>>>>>>>>>>>>>> Expected TokenB:", amountOut);
163
164    // 转移代币到配对合约
165    IERC20(tokenIn).transfer(pool, amountIn);
166
167    // 执行交换 - 根据tokenIn是token0还是token1来确定输出方向
168    uint256 amount0Out = tokenOut == _token0 ? amountOut : 0;
169    uint256 amount1Out = tokenOut == _token0 ? 0 : amountOut;
170
171    pair.swap(amount0Out, amount1Out, address(this), new bytes(0));
172  }
173
174  // 计算需要交换回去的数量
175  function _calculateAmountToSwapBack(
176    address pool,
177    address tokenIn,
178    uint256 amountOutNeeded
179  ) internal view returns (uint256 amountIn) {
180    IUniswapV2Pair pair = IUniswapV2Pair(pool); // poolA
181    (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
182
183    address _token0 = pair.token0();
184    bool token0IsTokenIn = tokenIn == _token0;
185
186    (uint112 reserveIn, uint112 reserveOut) = token0IsTokenIn ? (reserve0, reserve1) : (reserve1, reserve0);
187
188    amountIn = IUniswapV2Router02(uniswapV2Router).getAmountIn(amountOutNeeded, reserveIn, reserveOut);
189  }
190
191  // 计算还款数量(包含 0.3% 手续费)
192  function _calculateRepayAmount(uint256 amountBorrowed) internal pure returns (uint256) {
193    // Uniswap V2 手续费是 0.3%
194    return (amountBorrowed * 1000) / 997 + 1;
195  }
196
197  // 紧急提取函数
198  function emergencyWithdraw(address token) external onlyOwner {
199    uint256 balance = IERC20(token).balanceOf(address(this));
200    if (balance > 0) {
201      IERC20(token).transfer(owner, balance);
202    }
203  }
204}

@contracts/test/FlashSwap.t.sol:

  1// SPDX-License-Identifier: MIT
  2pragma solidity ^0.8.20;
  3
  4import "forge-std/Test.sol";
  5import "forge-std/console.sol";
  6import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
  7import "../src/FlashSwap.sol";
  8
  9// 为 Mock 合约添加的接口定义
 10interface IUniswapV2Callee {
 11    function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
 12}
 13
 14contract MockERC20 is ERC20 {
 15    constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
 16
 17    function mint(address to, uint256 amount) public {
 18        _mint(to, amount);
 19    }
 20
 21    function burn(address from, uint256 amount) public {
 22        _burn(from, amount);
 23    }
 24}
 25
 26contract MockUniswapV2Pair is IUniswapV2Pair {
 27    address public token0;
 28    address public token1;
 29    uint112 public reserve0;
 30    uint112 public reserve1;
 31    uint32 public blockTimestampLast;
 32
 33    constructor(address _token0, address _token1) {
 34        token0 = _token0;
 35        token1 = _token1;
 36        reserve0 = 0;
 37        reserve1 = 0;
 38        blockTimestampLast = uint32(block.timestamp);
 39    }
 40
 41    function setReserves(uint112 _reserve0, uint112 _reserve1) external {
 42        reserve0 = _reserve0;
 43        reserve1 = _reserve1;
 44        blockTimestampLast = uint32(block.timestamp);
 45    }
 46
 47    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
 48        return (reserve0, reserve1, blockTimestampLast);
 49    }
 50
 51    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external {
 52        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
 53        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
 54        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
 55
 56        uint balance0;
 57        uint balance1;
 58        { // scope for _token{0,1}, avoids stack too deep errors
 59            address _token0 = token0;
 60            address _token1 = token1;
 61            require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
 62            if (amount0Out > 0) MockERC20(token0).transfer(to, amount0Out); // optimistically transfer tokens
 63            if (amount1Out > 0) MockERC20(token1).transfer(to, amount1Out); // optimistically transfer tokens
 64            if (data.length > 0) {
 65                // 假设to地址实现了uniswapV2Call回调函数
 66                // 这个函数会被FlashSwap等合约实现,用于处理闪贷逻辑
 67                (bool success,) = to.call(abi.encodeWithSignature("uniswapV2Call(address,uint256,uint256,bytes)",
 68                    msg.sender, amount0Out, amount1Out, data));
 69                require(success, "Uniswap V2 callback failed");
 70            }
 71            balance0 = IERC20(_token0).balanceOf(address(this));
 72            balance1 = IERC20(_token1).balanceOf(address(this));
 73        }
 74        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
 75        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
 76        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
 77        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
 78            uint balance0Adjusted = balance0 * 1000 - amount0In * 3;
 79            uint balance1Adjusted = balance1 * 1000 - amount1In * 3;
 80            require(balance0Adjusted * balance1Adjusted >= uint(_reserve0) * _reserve1 * 1000 ** 2, 'UniswapV2: K');
 81        }
 82
 83        _update(balance0, balance1, _reserve0, _reserve1);
 84    }
 85
 86    function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
 87        uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32);
 88        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
 89        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
 90            // 仅测试,不做任何操作
 91        }
 92        reserve0 = uint112(balance0);
 93        reserve1 = uint112(balance1);
 94        blockTimestampLast = blockTimestamp;
 95    }
 96}
 97
 98contract MockUniswapV2Factory is IUniswapV2Factory {
 99    mapping(address => mapping(address => address)) public getPair;
100
101    function setPair(address tokenA, address tokenB, address pair) external {
102        getPair[tokenA][tokenB] = pair;
103        getPair[tokenB][tokenA] = pair; // Uniswap V2 工厂函数是双向对称的
104    }
105}
106
107contract MockUniswapV2Router is IUniswapV2Router02 {
108    // Uniswap V2 公式计算函数
109    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut) {
110        require(amountIn > 0, "UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT");
111        require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
112        uint amountInWithFee = amountIn * 997;
113        uint numerator = amountInWithFee * reserveOut;
114        uint denominator = (reserveIn * 1000) + amountInWithFee;
115        amountOut = numerator / denominator;
116    }
117
118    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn) {
119        require(amountOut > 0, "UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT");
120        require(reserveIn > 0 && reserveOut > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
121        uint numerator = reserveIn * amountOut * 1000;
122        uint denominator = (reserveOut - amountOut) * 997;
123        amountIn = (numerator / denominator) + 1;
124    }
125}
126
127contract FlashSwapTest is Test {
128    FlashSwap public flashSwap;
129    MockERC20 public tokenA;
130    MockERC20 public tokenB;
131    MockUniswapV2Pair public poolA;
132    MockUniswapV2Pair public poolB;
133    MockUniswapV2Factory public factory;
134    MockUniswapV2Router public router;
135
136    address public owner;
137
138    function setUp() public {
139        // 部署代币
140        tokenA = new MockERC20("Token A", "TKA");
141        tokenB = new MockERC20("Token B", "TKB");
142
143        // 部署工厂和路由器
144        factory = new MockUniswapV2Factory();
145        router = new MockUniswapV2Router();
146
147        // 部署配对合约
148        poolA = new MockUniswapV2Pair(address(tokenA), address(tokenB));
149        poolB = new MockUniswapV2Pair(address(tokenA), address(tokenB));
150
151        // 设置工厂中的配对信息
152        factory.setPair(address(tokenA), address(tokenB), address(poolA));
153
154        // 设置初始储备 (pairA: 1A = 1000B, 即 1:1000 的比例)
155        // 设置为 1000000 A 和 1000000000 B (比例为 1:1000)
156        tokenA.mint(address(poolA), 1000000);
157        tokenB.mint(address(poolA), 1000000000);
158        poolA.setReserves(1000000, 1000000000);
159
160        // 设置pairB的储备 (pairB: 1A = 1300B, 即 1:1300 的比例)
161        // 设置为 1000000 A 和 1300000000 B (比例为 1:1300)
162        tokenA.mint(address(poolB), 1000000);
163        tokenB.mint(address(poolB), 1300000000);
164        poolB.setReserves(1000000, 1300000000);
165
166        // 部署FlashSwapTestable合约
167        flashSwap = new FlashSwap();
168
169        // 设置模拟的工厂和路由器地址
170        flashSwap.setUniswapAddresses(address(factory), address(router));
171
172        // 将FlashSwapTestable合约的owner设置为测试合约(当前this)
173        // flashSwap合约的当前owner是部署合约时的msg.sender(也就是FlashSwapTest合约)
174        // 因此owner变量应该设置为FlashSwapTest合约的地址
175        owner = address(this);
176    }
177
178    function testFlashSwapArbitrage() public {
179        // 测试用例:验证FlashSwap套利交易逻辑
180        // 场景:pairA中1A=1000B, pairB中1A=1300B
181        // 从pairA借出来100个A,在pairB中用100个A兑换B代币,再在pairA中将B换成A还回去
182
183        // 记录初始状态
184        uint256 initialTokenABalance = tokenA.balanceOf(owner);
185        uint256 initialTokenBBalance = tokenB.balanceOf(owner);
186
187        console.log("Initial A Balance:", initialTokenABalance);
188        console.log("Initial B Balance:", initialTokenBBalance);
189
190        // 调用FlashSwap合约进行套利交易
191        // 从poolA借A代币,在poolB中进行套利
192        flashSwap.executeFlashSwap(
193            address(poolA),     // 从poolA借A代币
194            address(poolB),     // 在poolB中进行套利
195            address(tokenA),    // 借贷的代币是A
196            address(tokenB),    // 交换的目标代币是B
197            100                 // 借贷数量是100
198        );
199
200        // 检查是否获利
201        uint256 finalTokenABalance = tokenA.balanceOf(owner);
202        uint256 finalTokenBBalance = tokenB.balanceOf(owner);
203
204        console.log("Final A Balance:", finalTokenABalance);
205        console.log("Final B Balance:", finalTokenBBalance);
206        console.log("A Balance Change:", int(finalTokenABalance) - int(initialTokenABalance));
207        console.log("B Balance Change:", int(finalTokenBBalance) - int(initialTokenBBalance));
208
209        // 验证最终的B代币余额有所增加(套利获利)
210        // 初始B代币余额应该加上最终收益等于最终B代币余额
211        assertGt(finalTokenBBalance, initialTokenBBalance);
212
213        // 输出日志显示获利结果
214        console.log("B Token Profit:", finalTokenBBalance - initialTokenBBalance);
215    }
216}