You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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) {
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 12:12:43