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

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)。

这会引发两类问题:

  1. 当fee > 0时,合约尝试给零地址转ETH,要是合约本身余额不足(比如捐款没正确打进合约),transfer直接触发回滚;
  2. 就算fee == 0,transfer(0)虽然EVM允许,但后续逻辑可能因为权限检查不通过等问题触发回滚。

其他可能的触发原因

  1. 合约余额不足:如果捐款函数没把ETH转到合约里(比如直接打给了项目方),合约没有可转资金,transfer必然失败;
  2. 权限修饰器逻辑错误:authorisedPerson修饰器如果没正确验证调用者是对应项目的owner或合约管理员(因manager是零地址,只有项目方能通过检查,但如果调用者不是目标项目的owner,修饰器直接触发回滚);
  3. 零金额转账:如果fee为0,虽然transfer(0)合法,但部分环境会因无意义操作触发回滚,建议添加判断,仅当金额大于0时执行转账。

修复步骤

  1. 修正构造函数的赋值操作:
constructor(uint256 _platformFee) payable {
    manager = msg.sender; // 改为赋值操作
    platformFee = _platformFee;
    balances[msg.sender] = msg.value;
}
  1. 恢复给项目方转账的代码(你之前注释掉了),并用更安全的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, "转平台手续费失败");
}
  1. 检查authorisedPerson修饰器的逻辑,确保只有对应项目的owner或合约管理员能调用提款函数;
  2. 确认捐款函数确实把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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 17:25:02