Solidity提款函数正确编写及现有函数回滚问题排查
问题1:我的withdrawDonations函数为啥一直回滚?
核心bug排查
从你提供的代码和报错信息看,最关键的问题是构造函数里的赋值写错了:
constructor(uint256 _platformFee) payable { manager == msg.sender; // 这里是判断相等,不是赋值! platformFee = _platformFee; balances[msg.sender] = msg.value; }
manager == msg.sender 是布尔比较操作,根本没把部署合约的地址存到manager变量里,导致manager一直是默认的零地址address(0)。
这会引发两类问题:
- 当
fee > 0时,合约尝试给零地址转ETH,要是合约本身余额不足(比如捐款没正确打进合约),transfer直接触发回滚; - 就算
fee == 0,transfer(0)虽然EVM允许,但后续逻辑可能因为权限检查不通过等问题触发回滚。
其他可能的触发原因
- 合约余额不足:如果捐款函数没把ETH转到合约里(比如直接打给了项目方),合约没有可转资金,
transfer必然失败; - 权限修饰器逻辑错误:
authorisedPerson修饰器如果没正确验证调用者是对应项目的owner或合约管理员(因manager是零地址,只有项目方能通过检查,但如果调用者不是目标项目的owner,修饰器直接触发回滚); - 零金额转账:如果
fee为0,虽然transfer(0)合法,但部分环境会因无意义操作触发回滚,建议添加判断,仅当金额大于0时执行转账。
修复步骤
- 修正构造函数的赋值操作:
constructor(uint256 _platformFee) payable { manager = msg.sender; // 改为赋值操作 platformFee = _platformFee; balances[msg.sender] = msg.value; }
- 恢复给项目方转账的代码(你之前注释掉了),并用更安全的
call替代transfer:
uint256 ownerAmount = raisedAmount - fee; // 给项目方转钱 if (ownerAmount > 0) { (bool ownerSuccess, ) = payable(campaigns[_id].owner).call{value: ownerAmount}(""); require(ownerSuccess, "转钱给项目方失败"); } // 给平台转手续费 if (fee > 0) { (bool managerSuccess, ) = payable(manager).call{value: fee}(""); require(managerSuccess, "转平台手续费失败"); }
- 检查
authorisedPerson修饰器的逻辑,确保只有对应项目的owner或合约管理员能调用提款函数; - 确认捐款函数确实把ETH转入合约(比如
donateToCampaign中需更新campaigns[_id].amountCollected,同时保证合约实际收到ETH)。
问题2:Solidity里怎么正确写提款函数?
编写安全的提款函数要遵守以下核心规则:
1. 严格控制权限
用修饰器或内部函数验证调用者身份,比如仅允许合约管理员、项目归属者提款:
// 仅管理员可调用的修饰器 modifier onlyManager() { require(msg.sender == manager, "不是管理员"); _; } // 仅项目方或管理员可调用的修饰器 modifier onlyAuthorised(uint256 _id) { require(msg.sender == campaigns[_id].owner || msg.sender == manager, "无权限"); _; }
2. 遵循检查-更新-交互的顺序
先检查权限、金额合法性,再更新合约状态(比如清零可提款金额),最后执行转账,防止重入攻击:
function withdraw(uint256 _id) public onlyAuthorised(_id) { // 检查:确认有可提资金 (uint256 raisedAmount, uint256 fee) = calculatePlatformFee(_id); uint256 ownerAmount = raisedAmount - fee; require(raisedAmount > 0, "没有可提资金"); // 更新:先清零项目已筹金额,防止重复提款 campaigns[_id].amountCollected = 0; // 交互:执行转账 if (ownerAmount > 0) { (bool success, ) = payable(campaigns[_id].owner).call{value: ownerAmount}(""); require(success, "转钱给项目方失败"); } if (fee > 0) { (bool success, ) = payable(manager).call{value: fee}(""); require(success, "转平台手续费失败"); } // 发射事件,方便链上追踪 emit FundsWithdrawn(_id, raisedAmount, ownerAmount, fee, block.timestamp); }
3. 使用安全的转账方式
- 优先用
call{value: amount}("")代替transfer或send,因为后两者仅提供2300gas限制,若接收方是合约且需要更多gas处理逻辑,会直接导致转账失败; - 必须检查转账返回值,并添加明确的错误提示。
4. 避免溢出和零金额转账
- Solidity 0.8+版本自带溢出检查,但计算百分比(如平台费)时,注意计算顺序避免精度丢失;
- 添加判断逻辑,不执行零金额转账,减少不必要操作和潜在错误。
5. 发射事件记录操作
每次提款操作都要发射事件,方便链上审计和前端追踪:
event FundsWithdrawn(uint256 indexed campaignId, uint256 totalAmount, uint256 ownerAmount, uint256 platformFee, uint256 timestamp);
内容的提问来源于stack exchange,提问作者kihiuFrank
相关产品推荐
相关产品推荐

