Hardhat本地网络中MetaMask+Ethers.js调用合约Payable函数交易无理由回退
问题描述
我正在开发一款区块链dApp,支持客户通过扫描生成的二维码向超市转账ETH。技术栈为Solidity智能合约、Ethers.js前端交互、Hardhat本地网络+MetaMask钱包。目前非Payable功能正常,但调用payWithCode、sendMoneyToCustomer等Payable函数时,交易总是无理由回退。
Solidity合约代码
// SPDX-License-Identifier: MIT pragma solidity ^0.8.8; contract LoyaltyPoints { struct UserProfile { string username; string role; } mapping(address => UserProfile) private profiles; mapping(address => uint256) private balances; mapping(bytes32 => PurchaseRequest) private purchaseRequests; event PurchaseRequestGenerated(uint256 code); struct PurchaseRequest { address supermarket; uint256 amount; } modifier onlyRegisteredSupermarket() { UserProfile memory userProfile = profiles[msg.sender]; require(bytes(userProfile.username).length > 0, "Supermarket does not exist"); require(keccak256(bytes(userProfile.role)) == keccak256(bytes("supermarket")), "Not a Supermarket"); _; } modifier onlyRegisteredCustomer() { UserProfile memory userProfile = profiles[msg.sender]; require(bytes(userProfile.username).length > 0, "Customer does not exist"); require(keccak256(bytes(userProfile.role)) == keccak256(bytes("customer")), "Not a Customer"); _; } error InsufficientBalance(uint requested, uint available); function setProfile(string memory _username, string memory _role) public { profiles[msg.sender] = UserProfile(_username, _role); } function getProfile(address userAddress) public view returns (UserProfile memory) { return profiles[userAddress]; } // Function to generate a QR code (purchase request) for a customer to scan and pay function generatePurchaseRequest(uint256 amount) public onlyRegisteredSupermarket returns (uint256) { require(amount > 0, "Amount must be greater than zero"); uint256 code = uint256(keccak256(abi.encodePacked(msg.sender, amount, block.timestamp))) % 1000000; // Generate a 6-digit code purchaseRequests[bytes32(code)] = PurchaseRequest(msg.sender, amount); emit PurchaseRequestGenerated(code); return code; } // View function to get the purchase request details function getPurchaseRequestDetails(uint256 code) public view returns (address supermarket, uint256 amount) { bytes32 codeHash = bytes32(code); // Convert the code to bytes32 for lookup PurchaseRequest memory request = purchaseRequests[codeHash]; // Ensure the code is valid require(request.supermarket != address(0), "Invalid code"); return (request.supermarket, request.amount); } // Function for customer to scan the QR code and pay function payWithCode(uint256 code,address payable supermarketAddress) public payable onlyRegisteredCustomer { bytes32 codeHash = bytes32(code); // Convert the code to bytes32 for lookup PurchaseRequest memory request = purchaseRequests[codeHash]; // Ensure the code is valid require(request.supermarket != address(0), "Invalid code"); // Get the amount from the purchase request and ensure the customer sends the correct ETH uint256 amount = request.amount; // Transfer the amount to the supermarket balances[supermarketAddress] += amount; supermarketAddress.transfer(amount); // Remove the purchase request after payment delete purchaseRequests[codeHash]; } function sendMoneyToCustomer(address payable customer) public payable { balances[customer] += msg.value; customer.transfer(msg.value); } }
前端Ethers.js代码
import { defineStore } from "pinia"; import axios from "axios"; import { ethers } from 'ethers'; import { SHOPIFY_URL} from "~~/services/global.variables"; export const useShopBrandsStore = defineStore('shopBrands', { state: () => ({ name: "", full_name: "", my_profile : null, contractAddress: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", abi: [ { "inputs": [ { "internalType": "uint256", "name": "requested", "type": "uint256" }, { "internalType": "uint256", "name": "available", "type": "uint256" } ], "name": "InsufficientBalance", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", "name": "code", "type": "uint256" } ], "name": "PurchaseRequestGenerated", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "generatePurchaseRequest", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "userAddress", "type": "address" } ], "name": "getProfile", "outputs": [ { "components": [ { "internalType": "string", "name": "username", "type": "string" }, { "internalType": "string", "name": "role", "type": "string" } ], "internalType": "struct LoyaltyPoints.UserProfile", "name": "", "type": "tuple" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "code", "type": "uint256" } ], "name": "getPurchaseRequestDetails", "outputs": [ { "internalType": "address", "name": "supermarket", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "code", "type": "uint256" }, { "internalType": "address payable", "name": "supermarketAddress", "type": "address" } ], "name": "payWithCode", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [ { "internalType": "address payable", "name": "customer", "type": "address" } ], "name": "sendMoneyToCustomer", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_username", "type": "string" }, { "internalType": "string", "name": "_role", "type": "string" } ], "name": "setProfile", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ], }), actions: { async connectWallet () { console.log("Ethers:", ethers); const provider = new ethers.BrowserProvider(window.ethereum) console.log("Ethers:", ethers); console.log("Provider:", provider); await provider.send("eth_requestAccounts", []); const signer = await provider.getSigner(); const balance = await provider.getBalance(signer) const balanceInEth = ethers.formatEther(balance); console.log('s',this.balanceInEth) const walletAddress = await signer; // Save wallet address and provider in localStorage localStorage.setItem("signer", JSON.stringify(signer)); return { signer: signer, balanceInEth: balanceInEth }; }, async setProfile(signer, username, role) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); const tx = await contract.setProfile(username, role); await tx.wait(); }, async getProfile (signer) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); console.log('signer',signer) const userAddress = await signer; // Get the user's wallet address const profile = await contract.getProfile(userAddress); this.my_profile = profile return profile; }, async generatePurchaseRequest(signer, amount) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); const txResponse = await contract.generatePurchaseRequest(amount); // Wait for the transaction to be mined const txReceipt = await txResponse.wait(); // Now get the generated code from the transaction receipt const hexData = txReceipt.logs[0].data; console.log("txReceipt",txReceipt) const code = parseInt(hexData, 16); console.log("Generated code:", code) return code; }, // 649193 async checkPurchaseRequest(signer, code) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); // Retrieve purchase request details const [supermarket, amount] = await contract.getPurchaseRequestDetails(code); console.log('Supermarket:', supermarket); console.log('Amount:', ethers.formatEther(amount)); // Now the customer can confirm the payment return { supermarket, amount }; }, // async payWithCode(signer, code) { // const contract = new ethers.Contract(this.contractAddress, this.abi, signer); // // Fetch the request // const purchaseRequest = await contract.purchaseRequests(bytes32(code)); // console.log('p',await purchaseRequest) // const ethValue = ethers.parseEther('100'); // // Pay with code and the corresponding amount // const tx = await contract.payWithCode(code , {value: ethValue}// Ensure to send the correct ETH amount // ); // await tx.wait(); // Wait for the transaction to be mined // console.log('Payment successful:', tx); // }, async payWithCode(signer, code) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); // Fetch the request details first const { supermarket, amount } = await this.checkPurchaseRequest(signer, code); // Proceed with the payment console.log('address of supermarket',supermarket) const my_amount = ethers.parseEther(amount.toString()) console.log('my_account',my_amount) const tx = await contract.payWithCode(code,supermarket,{ value: my_amount }); await tx.wait(); // Wait for the transaction to be mined console.log('Payment successful:', tx); }, async sendMoneyToCustomer (signer,customerAddress,amount) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); console.log('ETH Amount:', amount); console.log('aaa',customerAddress) const ethValue = ethers.parseEther(amount); console.log('ETH in Wei:', ethValue); const tx = await contract.sendMoneyToCustomer(customerAddress, {value: ethValue}, // Convert the amount from ETH to Wei ); console.log('tx',tx) await tx.wait(); // Wait for the transaction to be mined }, // Whenever I use the payable function the transaction is reverted, saying reverted for no reason } });
问题分析与修复方案
一、Solidity合约核心问题修复
1. payWithCode函数问题
- 未校验用户实际发送的ETH金额(
msg.value)与订单请求金额是否匹配,金额不一致直接触发回退 - 冗余传入
supermarketAddress参数,易被篡改,应直接从订单请求中获取地址 transfer()方法仅提供2300gas,复杂环境下可能转账失败,改用call()提升兼容性
修复后的payWithCode:
function payWithCode(uint256 code) public payable onlyRegisteredCustomer { bytes32 codeHash = bytes32(code); PurchaseRequest memory request = purchaseRequests[codeHash]; require(request.supermarket != address(0), "Invalid code"); // 强制校验发送金额与订单金额一致 require(msg.value == request.amount, "Incorrect ETH amount sent"); address payable supermarketAddress = payable(request.supermarket); balances[supermarketAddress] += msg.value; // 使用call替代transfer,避免gas限制 (bool success, ) = supermarketAddress.call{value: msg.value}(""); require(success, "Transfer to supermarket failed"); delete purchaseRequests[codeHash]; }
2. sendMoneyToCustomer函数问题
- 无权限校验,任何人都可调用转钱,存在严重安全漏洞
- 同样存在
transfer()的gas限制问题
修复后的sendMoneyToCustomer:
// 仅注册超市可调用该函数 function sendMoneyToCustomer(address payable customer) public payable onlyRegisteredSupermarket { // 校验接收方为注册客户 UserProfile memory customerProfile = profiles[customer]; require(bytes(customerProfile.username).length > 0, "Customer does not exist"); require(keccak256(bytes(customerProfile.role)) == keccak256(bytes("customer")), "Not a registered customer"); balances[customer] += msg.value; (bool success, ) = customer.call{value: msg.value}(""); require(success, "Transfer to customer failed"); }
二、Ethers.js前端问题修复
1. payWithCode金额处理错误
checkPurchaseRequest返回的amount是wei单位的BigNumber,前端错误地将其转为字符串后用parseEther(),导致金额被错误放大(如1wei被转为1ETH)。
修复后的payWithCode:
async payWithCode(signer, code) { const contract = new ethers.Contract(this.contractAddress, this.abi, signer); const { supermarket, amount } = await this.checkPurchaseRequest(signer, code); // 直接使用wei单位的amount作为交易value const tx = await contract.payWithCode(code, { value: amount }); await tx.wait(); console.log('Payment successful:', tx); }
2. connectWallet序列化Signer错误
Signer对象无法被JSON序列化存入localStorage,需改为保存钱包地址,后续通过provider重新获取Signer:
修复后的connectWallet:
async connectWallet () { const provider = new ethers.BrowserProvider(window.ethereum) await provider.send("eth_requestAccounts", []); const signer = await provider.getSigner(); const walletAddress = await signer.getAddress(); const balance = await provider.getBalance(walletAddress); const balanceInEth = ethers.formatEther(balance); localStorage.setItem("walletAddress", walletAddress); return { signer: signer, balanceInEth: balanceInEth, walletAddress: walletAddress }; }
3. sendMoneyToCustomer调用参数冗余
调用时多传了一个逗号,导致overrides参数位置错误:
修复后的sendMoneyToCustomer:
async sendMoneyToCustomer(signer,customerAddress,amount) {
相关产品推荐
相关产品推荐

