简介 闪电贷(Flash Loan) 是一种在单笔区块链交易内借入资产、使用资产并在同一交易内归还借款(含手续费)的借贷模式。核心特点是 “原子性”:借款、使用和还款必须在同一笔交易内完成,否则交易会回滚,借款不会生效。
这种机制使得借款人在不提供抵押的情况下临时动用大量资产,用于套利、清算、头寸迁移等操作;同时也被用于发动复杂的攻击(如操纵价格、闪电清算等)。
原理与执行流程 典型单笔闪电贷交易包含三步(原子性):
借款(Borrow) 在交易开始阶段通过闪电贷提供方(Lending Pool/Router)请求借入若干资产。
使用(Use) 在同一交易中,用借入资金执行任意链上操作,例如在不同 DEX 之间套利、进行清算、调整抵押头寸、执行跨协议操作等。
还款(Repay) 在交易结束前,将借入金额加上借贷方要求的手续费一并偿还给闪电贷提供方。如果无法偿还(余额不足或逻辑失败),整笔交易 revert,等同于“未发生”。
关键点:闪电贷的安全性与“回滚语义”绑定;若中间步骤出错,借贷不会被实际放出和使用。
常见闪电贷提供方 Aave(flashLoan / flashLoanSimple 接口) DyDx(Solo or v3) Uniswap(flash swap:允许先输出资产,再在同一交易内履约) Balancer / Curve / 其它支持 flash 的池子 (实现细节与接口在不同协议间不同) 典型用途(Use cases) 跨 DEX 套利:利用不同交易所价格差套利,套利收益用于偿还贷款并留盈利 清算(Liquidation):借资产替别人清算欠债(避免自己需提前持有资产) 头寸迁移 / 债务重组:一笔交易内把抵押品替换、债务迁移到另一个协议或仓位 杠杆构建 / 扩展:临时放大仓位用于策略 原子化多步操作:把多个必须同时完成的步骤整合为一笔交易,避免中间状态风险 代码示例 // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.20; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { AssetToken } from "./AssetToken.sol"; import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Oracle } from "./Oracle.sol"; interface IFlashLoanReceiver { function executeOperation(address token, uint256 amount, uint256 fee, address initiator, bytes calldata params) external; } contract ThunderLoan is Ownable, Oracle { using SafeERC20 for IERC20; /*////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////*/ mapping(IERC20 => AssetToken) public s_tokenToAssetToken; mapping(IERC20 => bool) private s_currentlyFlashLoaning; uint256 private constant FEE_PRECISION = 1e18; uint256 private s_flashLoanFee = 3e15; // 0.3% /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ event Deposit(address indexed account, IERC20 indexed token, uint256 amount); event Withdraw(address indexed account, IERC20 indexed token, uint256 amount); event FlashLoan(address indexed receiver, IERC20 indexed token, uint256 amount, uint256 fee); event TokenAllowed(IERC20 indexed token, bool allowed); /*////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////*/ modifier notZero(uint256 amount) { require(amount > 0, "Amount cannot be zero"); _; } modifier allowedToken(IERC20 token) { require(address(s_tokenToAssetToken[token]) != address(0), "Token not allowed"); _; } /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ constructor(address oracle) Oracle(oracle) Ownable(msg.sender) {} function deposit(IERC20 token, uint256 amount) external notZero(amount) allowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; // 计算应铸造的资产代币数量 uint256 mintAmount = (amount * FEE_PRECISION) / assetToken.getExchangeRate(); // 铸造资产代币给用户 assetToken.mint(msg.sender, mintAmount); // 更新汇率(包含手续费) assetToken.updateExchangeRate(calculateFee(token, amount)); // 将底层代币转移到资产代币合约 token.safeTransferFrom(msg.sender, address(assetToken), amount); emit Deposit(msg.sender, token, amount); } function withdraw(IERC20 token, uint256 assetAmount) external notZero(assetAmount) allowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; // 计算可取回的底层代币数量 uint256 underlyingAmount = (assetAmount * assetToken.getExchangeRate()) / FEE_PRECISION; // 销毁用户的资产代币 assetToken.burn(msg.sender, assetAmount); // 将底层代币转给用户 assetToken.transferUnderlyingTo(msg.sender, underlyingAmount); emit Withdraw(msg.sender, token, underlyingAmount); } function flashLoan( address receiver, IERC20 token, uint256 amount, bytes calldata params ) external notZero(amount) allowedToken(token) { AssetToken assetToken = s_tokenToAssetToken[token]; // 检查合约余额是否足够 uint256 poolBalance = token.balanceOf(address(assetToken)); require(amount <= poolBalance, "Insufficient pool balance"); // 检查接收者是合约 require(receiver.code.length > 0, "Receiver must be contract"); // 计算手续费 uint256 fee = calculateFee(token, amount); // 更新汇率 assetToken.updateExchangeRate(fee); // 标记为正在闪电贷 s_currentlyFlashLoaning[token] = true; // 将资金转给接收者 assetToken.transferUnderlyingTo(receiver, amount); // 调用接收者的回调函数 IFlashLoanReceiver(receiver).executeOperation( address(token), amount, fee, msg.sender, params ); // 验证还款 uint256 newBalance = token.balanceOf(address(assetToken)); require(newBalance >= poolBalance + fee, "Flash loan not repaid"); // 重置闪电贷状态 s_currentlyFlashLoaning[token] = false; emit FlashLoan(receiver, token, amount, fee); } function repay(IERC20 token, uint256 amount) external { require(s_currentlyFlashLoaning[token], "Not in flash loan"); token.safeTransferFrom(msg.sender, address(s_tokenToAssetToken[token]), amount); } /*////////////////////////////////////////////////////////////// ADMIN FUNCTIONS //////////////////////////////////////////////////////////////*/ function setAllowedToken(IERC20 token, bool allowed) external onlyOwner { if (allowed) { require(address(s_tokenToAssetToken[token]) == address(0), "Already allowed"); string memory name = string.concat("ThunderLoan ", IERC20Metadata(address(token)).name()); string memory symbol = string.concat("tl", IERC20Metadata(address(token)).symbol()); AssetToken assetToken = new AssetToken(address(this), token, name, symbol); s_tokenToAssetToken[token] = assetToken; } else { delete s_tokenToAssetToken[token]; } emit TokenAllowed(token, allowed); } function setFlashLoanFee(uint256 newFee) external onlyOwner { require(newFee <= FEE_PRECISION, "Fee too high"); s_flashLoanFee = newFee; } /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ function calculateFee(IERC20 token, uint256 amount) public view returns (uint256) { uint256 tokenValue = (amount * getPriceInWeth(address(token))) / FEE_PRECISION; return (tokenValue * s_flashLoanFee) / FEE_PRECISION; } function isAllowedToken(IERC20 token) public view returns (bool) { return address(s_tokenToAssetToken[token]) != address(0); } function getFlashLoanFee() external view returns (uint256) { return s_flashLoanFee; } } 经济可行性与可行性检查 在执行闪电贷策略前需判断经济可行性,常见校验条目:
...