React Native DApp 开发全栈实战·从 0 到 1 系列(闪电贷合约-前端部分)

  • 木西
  • 发布于 9小时前
  • 阅读 69

前言延续《快速实现一个闪电贷智能合约》,这一篇直切“前端交互”。我们把「编译→部署→事件监听→异常捕获→Gas估算→余额刷新」等必踩深坑,压缩成3个可一键复现的用户故事:正常借100ETH,15秒内连本带息归还;部署“老赖”合约,故意不还,亲眼看交易被revert;极限测

前言

延续《快速实现一个闪电贷智能合约》,这一篇直切“前端交互”。
我们把「编译→部署→事件监听→异常捕获→Gas 估算→余额刷新」等必踩深坑,压缩成 3 个可一键复现的用户故事:

  1. 正常借 100 ETH,15 秒内连本带息归还;
  2. 部署“老赖”合约,故意不还,亲眼看交易被 revert;
  3. 极限测试:输入 0 或一次性借空池子,验证防御逻辑。

    前期准备

    • hardhat启动网络节点:npx hardhat node
    • 合约编译:npx hardhat compile 生成对应的xxx.json用获取abi等相关信息
    • 合约部署:npx hardhat deploy --tags token,FlashLoan,FlashLoanReceiver 获取合约地址(代币、借贷和借贷接收合约地址)
    • 节点的私钥导入钱包:用来与合约交互时支付对应的gas费

      恶意接收者合约

      说明:为测试借款不还场景,编写的合约包含编译、部署

      恶意不还款者合约

    • 合约
      
      // 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"]

* **相关指令**
  - 编译:npx hardhat compile
  - 部署: npx hardhat deploy --tags 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';

### 场景1(正常借贷流程)

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);

### 场景2(恶意借款不还)

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); } } }

#### 场景3(正常闪电贷的边界处理)
  - **无借贷(金额为0)**

核心代码

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/01128c5a5b6043f0bda38817bc851258~tplv-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/601501aa839c42bfacd2ed99e6cf510d~tplv-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>

# 总结
文章用“三段式”代码把闪电贷的前端交互拆成乐高:

-   准备阶段:Hardhat 本地链 + 合约编译部署脚本一次性启动,私钥导入 MetaMask 即可秒出 10 个富裕账户;
-   场景阶段:  
    – 正常流程演示了“先转手续费 → 发起 flashLoan → 监听事件 → 余额校验”完整闭环;  
    – 恶意接收者把还款逻辑注释掉,前端立刻捕获 revert,并解析 reason 字符串,让“不还钱就 revert”变得肉眼可见;  
    – 边界测试用 0 借款与超额借款两条用例,验证合约的 defensive 逻辑,同时展示 ethers 抛错与 revert reason 的抓取技巧。
-   交付物:三段可复制的 React/Vanilla 函数、四张效果截图、一条“console 即审计”的体验路径。

后续优化,也可以把池子换成 Aave、把代币换成 DAI,就能把同样的代码直接搬到主网 Fork 上继续“闪电”。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。