前言延续《快速实现一个闪电贷智能合约》,这一篇直切“前端交互”。我们把「编译→部署→事件监听→异常捕获→Gas估算→余额刷新」等必踩深坑,压缩成3个可一键复现的用户故事:正常借100ETH,15秒内连本带息归还;部署“老赖”合约,故意不还,亲眼看交易被revert;极限测
延续《快速实现一个闪电贷智能合约》,这一篇直切“前端交互”。
我们把「编译→部署→事件监听→异常捕获→Gas 估算→余额刷新」等必踩深坑,压缩成 3 个可一键复现的用户故事:
- 正常借 100 ETH,15 秒内连本带息归还;
- 部署“老赖”合约,故意不还,亲眼看交易被 revert;
- 极限测试:输入 0 或一次性借空池子,验证防御逻辑。
为测试借款不还场景,编写的合约包含编译、部署// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract FlashLoanReceiver1 {
// 闪电贷回调函数
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool) {
// 在这里执行闪电贷逻辑
// 还款
// uint256 amountToRepay = amount + premium;
// IERC20(asset).transfer(msg.sender, amountToRepay);
return true;
}
}
module.exports = async function ({getNamedAccounts,deployments}) {
const firstAccount = (await getNamedAccounts()).firstAccount;
const {deploy,log} = deployments;
const FlashLoanReceiver1=await deploy("FlashLoanReceiver1",{
contract: "FlashLoanReceiver1",
from: firstAccount,
args: [],//参数 owner
log: true,
// waitConfirmations: 1,
})
console.log("FlashLoanReceiver1合约地址 恶意不还款接收者",FlashLoanReceiver1.address)
}
module.exports.tags = ["all", "FlashLoanReceiver1"]
import { abi as FlashLoanABI } from '@/abi/FlashLoan.json';
import { abi as FlashLoanReceiverABI } from '@/abi/FlashLoanReceiver.json';
import { abi as FlashLoanReceiver1ABI } from '@/abi/FlashLoanReceiver1.json';
import { abi as MyTokenABI } from '@/abi/MyToken.json';
import * as ethers from 'ethers';
const FlashLoanFn= async()=>{
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []); // 唤起钱包
const signer = await provider.getSigner();
const FlashLoanAddress="0x7a2088a1bFc9d81c55368AE168C2C02570cB814F"
const FlashLoanReceiverAddress="0x09635F643e140090A9A8Dcd712eD6285858ceBef"
const MyTokenAddress="0x5FbDB2315678afecb367f032d93F642f64180aa3"
const FlashLoanContract = new ethers.Contract(FlashLoanAddress, FlashLoanABI, signer);
const FlashLoanReceiverContract = new ethers.Contract(FlashLoanReceiverAddress, FlashLoanReceiverABI, signer);
const MyTokenContract = new ethers.Contract(MyTokenAddress, MyTokenABI, signer);
console.log("完成借贷池子中的余额前",ethers.utils.formatEther(await MyTokenContract.balanceOf(FlashLoanAddress)))
const loanAmount = ethers.utils.parseEther("100"); // 100 ether
const fee = (loanAmount.mul(5)).div(10000);//0.05%手续费
console.log("loanAmount :", ethers.utils.formatEther(loanAmount), "ETH");
console.log("fee :", ethers.utils.formatEther(fee), "ETH");
/* ---------- 1. 给接收者转手续费 ---------- */
const tx1 = await MyTokenContract.transfer(FlashLoanReceiverAddress, fee);
await tx1.wait();
/* ---------- 2. 执行闪电贷 ---------- */
const tx2 = await FlashLoanContract.flashLoan(
FlashLoanReceiverAddress,
MyTokenAddress,
loanAmount,
"0x"
);
/* ---------- 3. 等待事件 ---------- */
const receipt = await tx2.wait();
const event = receipt.events?.find((e: any) => e.event === "FlashLoan");
console.log("完成借贷池子中的余额",ethers.utils.formatEther(await MyTokenContract.balanceOf(FlashLoanAddress)))
if (!event) throw new Error("FlashLoan 事件未触发");
console.log("✅ FlashLoan 事件参数:", event.args);
const FlashLoanFn=async ()=>{
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []); // 唤起钱包
const signer = await provider.getSigner();
const FlashLoanAddress="0x7a2088a1bFc9d81c55368AE168C2C02570cB814F"
const FlashLoanReceiverAddress="0x09635F643e140090A9A8Dcd712eD6285858ceBef"
const FlashLoanReceiver1Address="0xc5a5C42992dECbae36851359345FE25997F5C42d"//恶意不还款
const MyTokenAddress="0x5FbDB2315678afecb367f032d93F642f64180aa3"
const FlashLoanContract = new ethers.Contract(FlashLoanAddress, FlashLoanABI, signer);
const FlashLoanReceiverContract = new ethers.Contract(FlashLoanReceiverAddress, FlashLoanReceiverABI, signer);
//池子中有余额
await MyTokenContract.mint(
FlashLoanAddress,
ethers.utils.parseEther("1000")
);
console.log("前----",ethers.utils.formatEther(await MyTokenContract.balanceOf(FlashLoanAddress)))
const loanAmount = ethers.utils.parseEther("100"); // 100 ether
const fee = (loanAmount.mul(5)).div(10000);//0.05%手续费
console.log("loanAmount :", ethers.utils.formatEther(loanAmount), "ETH");
console.log("fee :", ethers.utils.formatEther(fee), "ETH");
/* ---------- 1. 给接收者转手续费 ---------- */
try {
// 池子余额已充足,但接收者不会还款 → 预期 revert
const tx = await FlashLoanContract.flashLoan(
FlashLoanReceiver1Address,
MyTokenAddress,
ethers.utils.parseEther("100"),
"0x"
);
await tx.wait(); // 如果走到这里,说明测试失败
alert("❌ 交易居然成功了,接收者应该还钱才对!");
} catch (err: any) {
// 捕获 revert 字符串
const reason = err.reason || err.message || "";
if (reason.includes("Flash loan not repaid")) {
alert("✅ 闪电贷成功 revert:贷款未偿还(符合预期)");
} else {
alert("⛔ 其他错误:" + reason);
}
}
}
# 核心代码
const tx = await FlashLoanContract.flashLoan(
FlashLoanReceiver1Address,
MyTokenAddress,
0,//借款为0
"0x"
);
# 核心代码
const hugeAmount = ethers.parseEther("10000");
const tx = await FlashLoanContract.flashLoan(
FlashLoanReceiver1Address,
MyTokenAddress,
hugeAmount,//超过池中的款项
"0x"
);
<div style="display:flex; gap:8px;flex-wrap:wrap;">
<img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/01128c5a5b6043f0bda38817bc851258tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757668998&x-orig-sign=hHmD%2FG2MtcIsmKEf9CDTHZaaaRA%3D" alt="图1转存失败,建议直接上传图片文件" width="200">
<img src="https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/601501aa839c42bfacd2ed99e6cf510dtplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pyo6KW_:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjQzNjE3MzQ5Njg0NTU0OSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1757668946&x-orig-sign=YbBCtr5%2FbIydJiDQQkD%2BFYvxRI4%3D" alt="图1转存失败,建议直接上传图片文件" width="200">
</div>
文章用“三段式”代码把闪电贷的前端交互拆成乐高:
后续优化,也可以把池子换成 Aave、把代币换成 DAI,就能把同样的代码直接搬到主网 Fork 上继续“闪电”。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码