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

Remix中Solidity合约已实现receive()但ETH余额显示为0排查

问题根因

核心问题出在你继承的OpenZeppelin PullPayment 合约逻辑,以及你对_asyncTransfer方法的错误调用:

  • PullPayment 是OpenZeppelin提供的托管式支付合约,设计目的是避免直接转账带来的重入风险:调用_asyncTransfer(payee, amount)时,不会把ETH留在业务合约内,而是会将对应金额的ETH自动转入独立的Escrow托管合约地址,仅在托管合约中记录payee地址对应的可提现额度,后续收款方需要主动调用提现方法,才能从托管合约把ETH取走。
  • 你在mint函数中写的_asyncTransfer(address(this), msg.value);存在两个问题:
    • 执行这行代码时,用户铸造支付的ETH会立刻从你的业务合约地址转到Escrow托管地址,因此你调用accountBalance()查询业务合约自身余额address(this).balance时,返回值永远是0。
    • 你错误将托管收款地址设为业务合约自身,等于把ETH锁进托管后,仅记录了业务合约地址有提现权限,但业务合约本身没有主动触发提现的逻辑,资金会卡在托管合约里。
  • 你实现的receive()函数不会在铸造流程中触发:receive()仅在用户不带任何调用数据(即裸转ETH)到合约地址时才会执行,用户铸造NFT是调用mint函数、携带函数选择器的交易,msg.value是直接随mint函数调用传入,不会走receive()逻辑。
修改方案

根据你是否需要保留PullPayment的托管能力,二选一即可:

方案1:不需要托管逻辑,直接让合约持有ETH(最常用的NFT铸造场景)

  • 移除合约继承列表里的PullPayment,删掉你重写的withdrawPayments函数,删掉mint函数里的_asyncTransfer(address(this), msg.value);这行错误代码。
  • 修改后用户支付的铸造款会直接留在业务合约地址,accountBalance()可以正常查询到余额。
  • 补充owner专属的提现函数即可,参考代码:
function withdraw() external onlyOwner {
    (bool success, ) = msg.sender.call{value: address(this).balance}("");
    require(success, "Withdraw failed");
}
  • 你之前写的receive()函数可以保留,用于接收用户误转的裸ETH。

方案2:需要保留PullPayment托管提现逻辑

  • 修正_asyncTransfer的收款地址,不要把钱打给合约自身,直接将收款方设为合约所有者即可,把mint里的对应代码改为:
_asyncTransfer(payable(owner()), msg.value);
  • 注意这种模式下你的业务合约余额永远是0,因为所有铸造款都会自动转到Escrow托管地址,你调用重写的withdrawPayments(payable(owner()))时,ETH会从托管地址直接转到owner地址,不会经过业务合约。如果要查询可提现金额,不能查业务合约自身余额,需要查询Escrow合约内记录的对应地址可提现额度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 01:30:53