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

众筹合约claimRefund函数Goerli测试网触发Panic代码17异常排查

众筹合约退款函数调用异常排查

问题背景

实现众筹智能合约的退款功能,要求众筹截止时间过后未达到目标且用户有捐赠记录时,退还用户对应捐赠金额。claimRefund函数在Remix IDE中运行正常,但通过thirdweb部署到Goerli测试网后调用时,持续触发回滚异常。

退款函数代码

function claimRefund(uint256 _id) public payable {
    Campaign storage campaign = campaigns[_id];

    require(campaign.deadline < block.timestamp, "The deadline has not passed yet.");
    require(campaign.amountCollected < campaign.target, "The campaign goal has been reached.");

    uint256 amount = donatedAmount[msg.sender];
    require(amount > 0, "No refund is available for this address.");

    donatedAmount[msg.sender] = 0;
    campaign.amountCollected -= amount;

    (bool sent, ) = payable(msg.sender).call{value: amount}("");
    require(sent, "Failed to send the refund.");
}

错误信息

call revert exception; VM Exception while processing transaction: reverted with panic code 17 [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="claimRefund(uint256)", data="0x4e487b710000000000000000000000000000000000000000000000000000000000000011", errorArgs=[{"type":"BigNumber","hex":"0x11"}], errorName="Panic", errorSignature="Panic(uint256)", reason=null, code=CALL_EXCEPTION, version=abi/5.7.0).

完整合约代码

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

contract CrowdFunding {
    struct Campaign {
        address owner;
        string title;
        string description;
        uint256 target;
        uint256 deadline;
        uint256 amountCollected;
         bool closed;
        string image;
        address[] donators;
        uint256[] donations;
      
    }
      mapping(address => uint256) public donatedAmount;

    mapping(uint256 => Campaign) public campaigns;

    uint256 public numberOfCampaigns = 0;

    function createCampaign(
        address _owner,
        string memory _title,
        string memory _description,
        uint256 _target,
        uint256 _deadline,
        string memory _image
    ) public returns (uint256) {
  
        Campaign storage campaign = campaigns[numberOfCampaigns];
        require(campaign.deadline < block.timestamp, "The deadline should be a date in the future.");
        campaign.owner = _owner;
        campaign.title = _title;
        campaign.description = _description;
        campaign.target = _target;
         campaign.deadline =   _deadline;
        campaign.amountCollected = 0;
        campaign.image = _image;

        numberOfCampaigns++;

        return numberOfCampaigns - 1;
    }

    function donateToCampaign(uint256 _id) public payable {
        Campaign storage campaign = campaigns[_id];
         require(!campaign.closed,"The campaign is disabled");
        require(campaign.deadline > block.timestamp, "The deadline has already passed.");

        campaign.donators.push(msg.sender);
        campaign.donations.push(msg.value);
        donatedAmount[msg.sender] += msg.value;
        campaign.amountCollected += msg.value;

        if (campaign.amountCollected >= campaign.target) {
           
            (bool sent,) = payable(campaign.owner).call{value: campaign.amountCollected}("");
            require(sent, "Transfer to campaign owner failed.");
                    campaign.closed = true;
        }
    }

    function claimRefund(uint256 _id) public payable {
        Campaign storage campaign = campaigns[_id];

        require(campaign.deadline < block.timestamp, "The deadline has not passed yet.");
        require(campaign.amountCollected < campaign.target, "The campaign goal has been reached.");

        uint256 amount = donatedAmount[msg.sender];
        require(amount > 0, "No refund is available for this address.");

        donatedAmount[msg.sender] = 0;
        campaign.amountCollected -= amount;

        (bool sent, ) = payable(msg.sender).call{value: amount}("");
        require(sent, "Failed to send the refund.");
    }

    function getDonators(uint256 _id) public view returns (address[] memory, uint256[] memory) {
        Campaign storage campaign = campaigns[_id];
        return (campaign.donators, campaign.donations);
    }

    function getCampaigns() public view returns (Campaign[] memory) {
        Campaign[] memory allCampaigns = new Campaign[](numberOfCampaigns);

        for (uint256 i = 0; i < numberOfCampaigns; i++) {
            Campaign storage item = campaigns[i];

            allCampaigns[i] = item;
        }

        return allCampaigns;
    }
}

异常原因排查

核心原因:全局捐赠额映射导致整数下溢

你的donatedAmount是全局mapping,记录用户在所有众筹项目中的总捐赠额,而非单个项目的捐赠额。当用户给多个项目捐赠后,调用某一个项目的claimRefund时,会尝试将用户的全部总捐赠额从该项目的amountCollected中扣除。若该项目的amountCollected小于用户总捐赠额,Solidity 0.8+的自动溢出检查会触发整数下溢,抛出Panic代码17(0x11)。

举个例子:用户给项目A捐1ETH,给项目B捐1ETH,donatedAmount[msg.sender]为2ETH。调用项目A的claimRefund时,项目A的amountCollected仅为1ETH,执行campaign.amountCollected -= amount即1-2,触发下溢,直接触发Panic回滚。

次要问题:claimRefund函数多余的payable修饰符

该函数不需要接收用户的ETH,添加payable会导致前端调用时可能误传ETH,但不是本次异常的直接原因,建议移除。

额外问题:createCampaign函数的无效截止时间检查

createCampaign中的require(campaign.deadline < block.timestamp, "The deadline should be a date in the future.");完全无效——此时campaign.deadline尚未赋值,默认值为0,0永远小于当前区块时间,无法阻止创建截止时间在过去的项目。正确写法应为检查传入的_deadline > block.timestamp。

修复方案

  1. 将捐赠额映射改为按项目区分的嵌套结构:
    替换原全局映射:

    mapping(uint256 => mapping(address => uint256)) public donatedAmount;
    
  2. 修改捐赠时的金额记录逻辑:
    在donateToCampaign函数中,将donatedAmount[msg.sender] += msg.value;改为:

    donatedAmount[_id][msg.sender] += msg.value;
    
  3. 修改退款时的金额获取逻辑:
    在claimRefund函数中,将uint256 amount = donatedAmount[msg.sender];改为:

    uint256 amount = donatedAmount[_id][msg.sender];
    

    同时移除claimRefund函数的payable修饰符。

  4. 修复创建项目时的截止时间检查:
    在createCampaign函数中,将require(campaign.deadline < block.timestamp, "The deadline should be a date in the future.");改为:

    require(_deadline > block.timestamp, "The deadline should be a date in the future.");
    

内容的提问来源于stack exchange,提问作者shuveksha gautam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 14:34:57