返回 SC 笔记
SC Day 36

Solidity - FlashLoan 合约 - 借→用→还+手续费

### 一、什么是 Flash Loan?

2026-05-06
第二阶段:框架实战
SolidityFlashLoanDeFiEIP3156ArbitrageProtocol

日期: 2026-05-06 方向: Solidity 阶段: 第二阶段:框架实战 标签: #Solidity #FlashLoan #DeFi #EIP3156 #ArbitrageProtocol


今日目标

  1. 深入理解 Flash Loan 的原子性原理(借→用→还必须在一笔交易内完成)
  2. 掌握 EIP-3156 Flash Loan 标准接口
  3. 了解主流 Flash Loan 提供者(Aave/dYdX/Uniswap)的实现差异
  4. 实现一个完整的 FlashLoan Pool 合约 + Borrower 合约
  5. 分析 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 V30.05% (协议可调)池子总流动性最大流动性,支持多资产
dYdX0 (免费)池子总流动性免费但可用代币有限
Uniswap V20.3%池子单边流动性Flash Swap(可借代币或ETH)
Uniswap V30 (只收 swap 费)单个 tick 范围的流动性flash() 函数
Balancer0Vault 中的流动性支持多代币同时借
Maker0DAI 铸造上限只支持 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%持续利息

常见误区

  1. 误区:Flash Loan 可以凭空创造资金

    • 事实:资金来自流动性池,如果交易结束时未归还,整笔交易回滚
  2. 误区:EOA(普通钱包)可以使用 Flash Loan

    • 事实:必须通过智能合约使用(需要实现 onFlashLoan 回调),普通用户需要部署合约
  3. 误区:Flash Loan 手续费是固定的

    • 事实:不同提供者费率不同。dYdX 和 Balancer 是免费的,Aave 收 0.05%
  4. 误区:Flash Loan 本身是攻击工具

    • 事实:Flash Loan 只是一个工具,套利和清算是合法用途。攻击通常利用的是目标协议的漏洞(如依赖即时价格的预言机)
  5. 误区:Flash Loan 套利一定赚钱

    • 事实:MEV 竞争激烈,大部分套利机会被 searcher 用 Flashbots 抢走,Gas 竞价可能导致亏损

面试关联

Q1: 解释 Flash Loan 的工作原理

简短回答:Flash Loan 利用 EVM 的交易原子性,在一笔交易内完成借-用-还的全流程。如果借款者未在交易结束前归还资金+手续费,整笔交易自动回滚,资金安全。

详细回答

  1. 借款者合约调用 Flash Loan Pool 的 flashLoan 函数
  2. Pool 转出代币给借款者
  3. Pool 调用借款者的 onFlashLoan 回调函数
  4. 借款者在回调中使用资金(套利、清算等)
  5. 借款者 approve Pool 扣回本金+手续费
  6. Pool 验证余额恢复 + 手续费到账
  7. 如果任何步骤失败,EVM 回滚整笔交易

Q2: Flash Loan 攻击是怎么发生的?如何防御?

攻击模式

  1. 借大量代币 → 在 DEX 大量买入/卖出 → 操纵价格
  2. 利用目标协议依赖的即时价格(如 DEX spot price)获利
  3. 归还 Flash Loan + 手续费,利润留在攻击者合约

防御方法

  • 使用 TWAP(时间加权平均价格)而非即时价格
  • 使用 Chainlink 等外部预言机
  • 限制单笔交易内的操作量
  • 使用 commit-reveal 方案延迟价格敏感操作

Q3: 比较 Aave、dYdX、Uniswap 的 Flash Loan 实现

维度AavedYdXUniswap V2
费率0.05%0%0.3%
回调方式executeOperationcallFunctionuniswapV2Call
多资产支持有限配对中的两种
标准类 EIP-3156自定义Flash Swap

参考资源

  1. EIP-3156: Flash Loans — 标准规范
  2. Aave V3 Flash Loans — Aave 实现
  3. Uniswap V2 Flash Swaps — Uniswap 实现
  4. Rekt News — Flash Loan 攻击案例分析
  5. Flashbots — MEV 保护和套利基础设施
  6. Solidity by Example - Flash Loan — 简化教程