SC Day 36
Solidity - FlashLoan 合约 - 借→用→还+手续费
### 一、什么是 Flash Loan?
2026-05-06
第二阶段:框架实战SolidityFlashLoanDeFiEIP3156ArbitrageProtocol
日期: 2026-05-06 方向: Solidity 阶段: 第二阶段:框架实战 标签: #Solidity #FlashLoan #DeFi #EIP3156 #ArbitrageProtocol
今日目标
- 深入理解 Flash Loan 的原子性原理(借→用→还必须在一笔交易内完成)
- 掌握 EIP-3156 Flash Loan 标准接口
- 了解主流 Flash Loan 提供者(Aave/dYdX/Uniswap)的实现差异
- 实现一个完整的 FlashLoan Pool 合约 + Borrower 合约
- 分析 Flash Loan 攻击向量(价格操纵)
核心概念
一、什么是 Flash Loan?
Flash Loan(闪电贷)是 DeFi 独有的创新——无需抵押,借任意金额的资金,只要在同一笔交易内归还本金+手续费。
传统借贷:
1. 提供抵押品 → 2. 借款 → 3. 使用资金 → 4. 到期还款
时间跨度:天/周/月
Flash Loan:
一笔交易内完成全部流程:
┌─────────────────────────────────────────┐
│ 1. 从池子借出 1,000,000 USDC │
│ 2. 用这些 USDC 做套利/清算/... │
│ 3. 归还 1,000,000 USDC + 900 手续费 │
│ │
│ → 如果第 3 步失败 → 整笔交易 revert │
│ → 就好像什么都没发生过 │
└─────────────────────────────────────────┘
为什么这是安全的?
EVM 的原子性保证:
一笔交易要么全部成功,要么全部回滚
Flash Loan 合约的逻辑:
1. 记录借出前的余额 (balanceBefore)
2. 转出代币给借款者
3. 调用借款者的回调函数
4. 检查: balanceAfter >= balanceBefore + fee
5. 如果检查失败 → revert → 资金从未离开池子
二、Flash Loan 的使用场景
| 场景 | 说明 | 利润来源 |
|---|---|---|
| 套利 | 利用不同 DEX 之间的价差 | 价差 - Gas - 手续费 |
| 清算 | 借款还清他人的欠债,获取清算奖金 | 清算奖金 - 手续费 |
| 抵押品互换 | 借出资金还清贷款 → 取回抵押品 → 换成新资产 → 重新借贷 | 节省操作步骤 |
| 自清算 | 借款还清自己的欠债,避免被惩罚清算 | 避免清算罚金 |
| 治理攻击 | 借大量治理代币 → 投票 → 还款 | (恶意用途) |
三、EIP-3156: Flash Loan 标准
// ===== Lender Interface =====
interface IERC3156FlashLender {
/// 返回支持的最大借款量
function maxFlashLoan(address token) external view returns (uint256);
/// 返回指定借款量的手续费
function flashFee(address token, uint256 amount) external view returns (uint256);
/// 执行 Flash Loan
function flashLoan(
IERC3156FlashBorrower receiver, // 借款者合约
address token, // 借什么代币
uint256 amount, // 借多少
bytes calldata data // 传递给借款者的额外数据
) external returns (bool);
}
// ===== Borrower Interface =====
interface IERC3156FlashBorrower {
/// Flash Loan 回调函数
/// @return keccak256("ERC3156FlashBorrower.onFlashLoan") 作为成功确认
function onFlashLoan(
address initiator, // 发起者(谁调用了 flashLoan)
address token, // 借的什么代币
uint256 amount, // 借了多少
uint256 fee, // 手续费
bytes calldata data // 额外数据
) external returns (bytes32);
}
四、主流 Flash Loan 提供者对比
| 提供者 | 手续费 | 最大借款量 | 特点 |
|---|---|---|---|
| Aave V3 | 0.05% (协议可调) | 池子总流动性 | 最大流动性,支持多资产 |
| dYdX | 0 (免费) | 池子总流动性 | 免费但可用代币有限 |
| Uniswap V2 | 0.3% | 池子单边流动性 | Flash Swap(可借代币或ETH) |
| Uniswap V3 | 0 (只收 swap 费) | 单个 tick 范围的流动性 | flash() 函数 |
| Balancer | 0 | Vault 中的流动性 | 支持多代币同时借 |
| Maker | 0 | DAI 铸造上限 | 只支持 DAI |
五、Flash Loan 执行流程详解
用户(EOA) → FlashLoanBorrower(合约) → FlashLoanPool(合约) → DEX/Lending(目标)
详细流程:
1. 用户调用 Borrower.executeFlashLoan(amount)
│
2. Borrower 调用 Pool.flashLoan(borrower, token, amount, data)
│
3. Pool 执行:
├── a. 记录 balanceBefore
├── b. 转出 token 给 Borrower
├── c. 调用 Borrower.onFlashLoan(initiator, token, amount, fee, data)
│ │
│ └── Borrower 在回调中:
│ ├── 用借到的资金做套利/清算/...
│ ├── 确保自己有 amount + fee 的代币
│ └── approve Pool 扣款(或直接转账)
│
├── d. 从 Borrower 收回 amount + fee
├── e. 验证 balanceAfter >= balanceBefore + fee
└── f. 如果验证失败 → revert 整笔交易
代码实战
Flash Loan Pool 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title FlashLoanPool
* @notice 简化版 Flash Loan 池,兼容 EIP-3156 接口
* @dev 支持存入流动性 + 提供闪电贷
*/
interface IFlashBorrower {
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32);
}
contract FlashLoanPool is ReentrancyGuard {
using SafeERC20 for IERC20;
// ===== 常量 =====
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
uint256 public constant FEE_BPS = 9; // 0.09% 手续费 (基点)
uint256 public constant BPS_DENOMINATOR = 10000;
// ===== 状态变量 =====
IERC20 public immutable token; // 支持的代币
address public owner; // 管理员
uint256 public totalDeposited; // 总存入量
mapping(address => uint256) public deposits; // 流动性提供者的存款
uint256 public totalFeesCollected; // 累计手续费收入
// ===== 事件 =====
event Deposited(address indexed provider, uint256 amount);
event Withdrawn(address indexed provider, uint256 amount);
event FlashLoan(
address indexed borrower,
address indexed initiator,
uint256 amount,
uint256 fee
);
// ===== 错误 =====
error InsufficientLiquidity(uint256 requested, uint256 available);
error CallbackFailed();
error RepaymentFailed(uint256 expected, uint256 actual);
error ZeroAmount();
error InsufficientDeposit();
constructor(address _token) {
token = IERC20(_token);
owner = msg.sender;
}
// ===== 流动性管理 =====
/**
* @notice 存入代币提供流动性
*/
function deposit(uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
token.safeTransferFrom(msg.sender, address(this), amount);
deposits[msg.sender] += amount;
totalDeposited += amount;
emit Deposited(msg.sender, amount);
}
/**
* @notice 取出存入的代币
*/
function withdraw(uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
if (deposits[msg.sender] < amount) revert InsufficientDeposit();
deposits[msg.sender] -= amount;
totalDeposited -= amount;
token.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
// ===== Flash Loan 核心 =====
/**
* @notice 返回可借的最大数量
*/
function maxFlashLoan() external view returns (uint256) {
return token.balanceOf(address(this));
}
/**
* @notice 计算手续费
* @param amount 借款数量
* @return fee 手续费
*/
function flashFee(uint256 amount) public pure returns (uint256) {
return (amount * FEE_BPS) / BPS_DENOMINATOR;
}
/**
* @notice 执行 Flash Loan
* @param borrower 借款者合约地址
* @param amount 借款数量
* @param data 传递给借款者的额外数据
*/
function flashLoan(
IFlashBorrower borrower,
uint256 amount,
bytes calldata data
) external nonReentrant returns (bool) {
if (amount == 0) revert ZeroAmount();
uint256 balanceBefore = token.balanceOf(address(this));
if (amount > balanceBefore) {
revert InsufficientLiquidity(amount, balanceBefore);
}
uint256 fee = flashFee(amount);
// 步骤 1: 转出代币给借款者
token.safeTransfer(address(borrower), amount);
// 步骤 2: 调用借款者的回调函数
bytes32 result = borrower.onFlashLoan(
msg.sender, // initiator
address(token), // token
amount, // amount
fee, // fee
data // data
);
// 步骤 3: 验证回调返回值
if (result != CALLBACK_SUCCESS) {
revert CallbackFailed();
}
// 步骤 4: 收回本金 + 手续费
// 借款者需要先 approve
token.safeTransferFrom(address(borrower), address(this), amount + fee);
// 步骤 5: 验证余额
uint256 balanceAfter = token.balanceOf(address(this));
if (balanceAfter < balanceBefore + fee) {
revert RepaymentFailed(balanceBefore + fee, balanceAfter);
}
totalFeesCollected += fee;
emit FlashLoan(address(borrower), msg.sender, amount, fee);
return true;
}
/**
* @notice 查看池子信息
*/
function poolInfo() external view returns (
uint256 liquidity,
uint256 feeBps,
uint256 feesCollected
) {
return (
token.balanceOf(address(this)),
FEE_BPS,
totalFeesCollected
);
}
}
Flash Loan Borrower 合约(套利示例)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IFlashBorrower {
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32);
}
interface IFlashLoanPool {
function flashLoan(
IFlashBorrower borrower,
uint256 amount,
bytes calldata data
) external returns (bool);
function flashFee(uint256 amount) external view returns (uint256);
}
// 模拟 DEX 接口
interface ISimpleDEX {
function swap(address tokenIn, address tokenOut, uint256 amountIn) external returns (uint256 amountOut);
function getAmountOut(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256);
}
/**
* @title FlashLoanArbitrage
* @notice 使用 Flash Loan 进行 DEX 间套利的示例合约
*/
contract FlashLoanArbitrage is IFlashBorrower {
using SafeERC20 for IERC20;
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
IFlashLoanPool public immutable flashPool;
address public owner;
// ===== 错误 =====
error NotPool();
error NotOwner();
error NotProfitable(uint256 cost, uint256 revenue);
error UnauthorizedInitiator();
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
constructor(address _flashPool) {
flashPool = IFlashLoanPool(_flashPool);
owner = msg.sender;
}
/**
* @notice 发起套利交易
* @param amount Flash Loan 数量
* @param dexA 第一个 DEX
* @param dexB 第二个 DEX
* @param tokenA 代币 A(借的代币)
* @param tokenB 代币 B(中间代币)
*/
function executeArbitrage(
uint256 amount,
address dexA,
address dexB,
address tokenA,
address tokenB
) external onlyOwner {
// 编码套利参数传递给回调
bytes memory data = abi.encode(dexA, dexB, tokenA, tokenB);
// 发起 Flash Loan
flashPool.flashLoan(
IFlashBorrower(address(this)),
amount,
data
);
}
/**
* @notice Flash Loan 回调 — 在这里执行套利逻辑
*/
function onFlashLoan(
address initiator,
address token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override returns (bytes32) {
// 安全检查 1: 只接受来自 Flash Pool 的调用
if (msg.sender != address(flashPool)) revert NotPool();
// 安全检查 2: 只接受自己发起的 Flash Loan
if (initiator != address(this)) revert UnauthorizedInitiator();
// 解码套利参数
(address dexA, address dexB, address tokenA, address tokenB) =
abi.decode(data, (address, address, address, address));
// ===== 套利逻辑 =====
// 步骤 1: 在 DEX A 上用 tokenA 换 tokenB
IERC20(tokenA).approve(dexA, amount);
uint256 tokenBReceived = ISimpleDEX(dexA).swap(tokenA, tokenB, amount);
// 步骤 2: 在 DEX B 上用 tokenB 换回 tokenA
IERC20(tokenB).approve(dexB, tokenBReceived);
uint256 tokenAReceived = ISimpleDEX(dexB).swap(tokenB, tokenA, tokenBReceived);
// ===== 验证盈利 =====
uint256 totalCost = amount + fee;
if (tokenAReceived <= totalCost) {
revert NotProfitable(totalCost, tokenAReceived);
}
uint256 profit = tokenAReceived - totalCost;
// ===== 授权 Pool 收回本金+手续费 =====
IERC20(token).approve(address(flashPool), amount + fee);
// 利润留在合约中,owner 可以提取
emit ArbitrageExecuted(amount, profit, fee);
return CALLBACK_SUCCESS;
}
/**
* @notice 提取利润
*/
function withdrawProfit(address _token) external onlyOwner {
uint256 balance = IERC20(_token).balanceOf(address(this));
if (balance > 0) {
IERC20(_token).safeTransfer(owner, balance);
}
}
/**
* @notice 检查套利机会(只读,不执行)
*/
function checkArbitrage(
uint256 amount,
address dexA,
address dexB,
address tokenA,
address tokenB
) external view returns (bool profitable, uint256 expectedProfit) {
uint256 fee = flashPool.flashFee(amount);
// 模拟: DEX A 价格
uint256 tokenBAmount = ISimpleDEX(dexA).getAmountOut(tokenA, tokenB, amount);
// 模拟: DEX B 价格
uint256 tokenABack = ISimpleDEX(dexB).getAmountOut(tokenB, tokenA, tokenBAmount);
uint256 totalCost = amount + fee;
if (tokenABack > totalCost) {
return (true, tokenABack - totalCost);
} else {
return (false, 0);
}
}
// ===== 事件 =====
event ArbitrageExecuted(uint256 flashAmount, uint256 profit, uint256 fee);
}
测试合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/FlashLoanPool.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// 模拟 ERC20
contract MockToken is ERC20 {
constructor() ERC20("Mock USDC", "USDC") {
_mint(msg.sender, 10_000_000e18);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
// 简单的借款者:借了就还
contract SimpleBorrower is IFlashBorrower {
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
function onFlashLoan(
address,
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external override returns (bytes32) {
// 借到钱了!在这里可以做任何操作...
// 最后授权 pool 收回
IERC20(token).approve(msg.sender, amount + fee);
return CALLBACK_SUCCESS;
}
}
// 恶意借款者:不还钱
contract MaliciousBorrower is IFlashBorrower {
function onFlashLoan(
address, address, uint256, uint256, bytes calldata
) external override returns (bytes32) {
// 不 approve,不还钱!
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}
contract FlashLoanPoolTest is Test {
FlashLoanPool public pool;
MockToken public usdc;
SimpleBorrower public borrower;
MaliciousBorrower public badBorrower;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
usdc = new MockToken();
pool = new FlashLoanPool(address(usdc));
borrower = new SimpleBorrower();
badBorrower = new MaliciousBorrower();
// Alice 提供 1M USDC 流动性
usdc.transfer(alice, 1_000_000e18);
vm.startPrank(alice);
usdc.approve(address(pool), 1_000_000e18);
pool.deposit(1_000_000e18);
vm.stopPrank();
// 给借款者一些手续费资金
usdc.transfer(address(borrower), 10_000e18);
}
function test_FlashLoan_Success() public {
uint256 borrowAmount = 100_000e18;
uint256 expectedFee = pool.flashFee(borrowAmount);
// 0.09% of 100,000 = 90
uint256 poolBalanceBefore = usdc.balanceOf(address(pool));
vm.prank(bob);
bool success = pool.flashLoan(
IFlashBorrower(address(borrower)),
borrowAmount,
""
);
assertTrue(success);
// 池子余额应该增加了手续费
uint256 poolBalanceAfter = usdc.balanceOf(address(pool));
assertEq(poolBalanceAfter, poolBalanceBefore + expectedFee);
// 累计手续费
assertEq(pool.totalFeesCollected(), expectedFee);
}
function test_FlashLoan_RevertsWhen_NotRepaid() public {
vm.prank(bob);
vm.expectRevert(); // SafeERC20 的 transferFrom 会失败
pool.flashLoan(
IFlashBorrower(address(badBorrower)),
100_000e18,
""
);
}
function test_FlashLoan_RevertsWhen_InsufficientLiquidity() public {
vm.prank(bob);
vm.expectRevert(
abi.encodeWithSelector(
FlashLoanPool.InsufficientLiquidity.selector,
2_000_000e18,
1_000_000e18
)
);
pool.flashLoan(
IFlashBorrower(address(borrower)),
2_000_000e18,
""
);
}
function test_FlashFee_Calculation() public view {
// 0.09% = 9 / 10000
assertEq(pool.flashFee(10000e18), 9e18);
assertEq(pool.flashFee(1_000_000e18), 900e18);
assertEq(pool.flashFee(1e18), 9e14); // 0.0009e18
}
function testFuzz_FlashLoan(uint256 amount) public {
vm.assume(amount > 0 && amount <= 1_000_000e18);
uint256 fee = pool.flashFee(amount);
// 确保借款者有足够手续费
usdc.mint(address(borrower), fee);
vm.prank(bob);
bool success = pool.flashLoan(
IFlashBorrower(address(borrower)),
amount,
""
);
assertTrue(success);
}
}
关键要点总结
Flash Loan 核心设计
安全保障链:
1. 原子性: EVM 交易要么全部成功,要么全部回滚
2. 余额检查: 交易结束时验证 balanceAfter >= balanceBefore + fee
3. 回调确认: 验证 onFlashLoan 返回正确的 magic value
4. 重入保护: nonReentrant 防止嵌套攻击
Flash Loan 经济模型
提供者收益:
手续费收入(对池子存款人)
0.05%-0.3% 的无风险收益
借款者收益:
套利利润 - Gas 费 - Flash Loan 手续费
通常需要大量资金才能覆盖成本
风险分析:
提供者: 几乎零风险(要么收到手续费,要么交易回滚)
借款者: 风险是 Gas 费浪费(如果交易 revert)
Flash Loan vs 传统借贷
| 维度 | Flash Loan | 传统 DeFi 借贷 |
|---|---|---|
| 抵押 | 无需抵押 | 超额抵押 |
| 时间 | 同一笔交易 | 无限期 |
| 金额 | 池子全部流动性 | 受抵押率限制 |
| 用户 | 只有智能合约 | EOA 也可以 |
| 风险 | 零(对提供者) | 清算风险 |
| 手续费 | 一次性 0.05-0.3% | 持续利息 |
常见误区
-
误区:Flash Loan 可以凭空创造资金
- 事实:资金来自流动性池,如果交易结束时未归还,整笔交易回滚
-
误区:EOA(普通钱包)可以使用 Flash Loan
- 事实:必须通过智能合约使用(需要实现 onFlashLoan 回调),普通用户需要部署合约
-
误区:Flash Loan 手续费是固定的
- 事实:不同提供者费率不同。dYdX 和 Balancer 是免费的,Aave 收 0.05%
-
误区:Flash Loan 本身是攻击工具
- 事实:Flash Loan 只是一个工具,套利和清算是合法用途。攻击通常利用的是目标协议的漏洞(如依赖即时价格的预言机)
-
误区:Flash Loan 套利一定赚钱
- 事实:MEV 竞争激烈,大部分套利机会被 searcher 用 Flashbots 抢走,Gas 竞价可能导致亏损
面试关联
Q1: 解释 Flash Loan 的工作原理
简短回答:Flash Loan 利用 EVM 的交易原子性,在一笔交易内完成借-用-还的全流程。如果借款者未在交易结束前归还资金+手续费,整笔交易自动回滚,资金安全。
详细回答:
- 借款者合约调用 Flash Loan Pool 的 flashLoan 函数
- Pool 转出代币给借款者
- Pool 调用借款者的 onFlashLoan 回调函数
- 借款者在回调中使用资金(套利、清算等)
- 借款者 approve Pool 扣回本金+手续费
- Pool 验证余额恢复 + 手续费到账
- 如果任何步骤失败,EVM 回滚整笔交易
Q2: Flash Loan 攻击是怎么发生的?如何防御?
攻击模式:
- 借大量代币 → 在 DEX 大量买入/卖出 → 操纵价格
- 利用目标协议依赖的即时价格(如 DEX spot price)获利
- 归还 Flash Loan + 手续费,利润留在攻击者合约
防御方法:
- 使用 TWAP(时间加权平均价格)而非即时价格
- 使用 Chainlink 等外部预言机
- 限制单笔交易内的操作量
- 使用 commit-reveal 方案延迟价格敏感操作
Q3: 比较 Aave、dYdX、Uniswap 的 Flash Loan 实现
| 维度 | Aave | dYdX | Uniswap V2 |
|---|---|---|---|
| 费率 | 0.05% | 0% | 0.3% |
| 回调方式 | executeOperation | callFunction | uniswapV2Call |
| 多资产 | 支持 | 有限 | 配对中的两种 |
| 标准 | 类 EIP-3156 | 自定义 | Flash Swap |
参考资源
- EIP-3156: Flash Loans — 标准规范
- Aave V3 Flash Loans — Aave 实现
- Uniswap V2 Flash Swaps — Uniswap 实现
- Rekt News — Flash Loan 攻击案例分析
- Flashbots — MEV 保护和套利基础设施
- Solidity by Example - Flash Loan — 简化教程