众筹合约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。
修复方案
将捐赠额映射改为按项目区分的嵌套结构:
替换原全局映射:mapping(uint256 => mapping(address => uint256)) public donatedAmount;修改捐赠时的金额记录逻辑:
在donateToCampaign函数中,将donatedAmount[msg.sender] += msg.value;改为:donatedAmount[_id][msg.sender] += msg.value;修改退款时的金额获取逻辑:
在claimRefund函数中,将uint256 amount = donatedAmount[msg.sender];改为:uint256 amount = donatedAmount[_id][msg.sender];同时移除
claimRefund函数的payable修饰符。修复创建项目时的截止时间检查:
在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

