基于Uniswap V2的闪电贷套利合约实现与分析

前言:闪电贷在 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 状态变量与事件机制
合约的状态变量设计简洁高效:
uniswapV2Factory和uniswapV2Router:存储协议地址,支持测试时的动态配置。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}
