Solidity中payable函数内msg.sender.call在不同调用场景下的行为差异问题排查及解惑
解答你的Solidity重入测试疑问
嘿,我来帮你拆解下遇到的问题,搞清楚两种调用行为差异的核心原因,顺便解答你关于payable函数和call的疑问:
一、两种调用行为差异的核心原因
你碰到的情况,本质是调用场景下的合约ETH余额状态和攻击合约的回调函数实现这两个点在影响结果:
1. Remix手动调用为啥没问题?
当你在Remix里直接调用修改后的deposit时,肯定是给合约转入了至少1 ETH(甚至更多),这时候合约地址本身有足够的ETH来执行msg.sender.call{value: 1 ether}("")。而且你的外部账户(EOA)接收ETH根本不需要任何回调函数,所以这个call会顺利完成,sent返回true,函数自然能跑完。
2. 攻击合约调用为啥失败?
而攻击合约调用时,大概率是以下两种情况之一:
- 攻击合约的回调函数没加
payable:如果攻击合约的fallback或者receive函数没有payable修饰符,那当EtherStore合约给它转ETH时,它根本接不住,直接导致call失败,sent返回false,触发require报错终止执行。这是最常见的原因。 - 合约余额+gas的隐性限制:如果攻击合约调用
deposit时只转了刚好1 ETH,合约收到后余额是1 ETH,紧接着要转1 ETH出去。虽然余额数值够,但EVM执行转账需要预留一定gas,如果剩余gas不足,也可能导致call失败。不过这种情况概率比第一种低很多。
二、关于“带payable修饰符的函数能不能执行msg.sender.call{}”的疑问
明确给你答案:完全可以。payable修饰符只是标记这个函数能接收ETH,和函数内部执行call转账操作半毛钱冲突都没有。只要合约有足够的ETH余额,并且接收方(这里是msg.sender)能接收ETH(EOA自动可以,合约需要带payable的回调函数),call就能正常跑起来。
补充:你想验证的“单次deposit发起直接攻击”思路
如果你要验证这种场景下的重入攻击,得注意这几点:
- 攻击合约的
fallback/receive必须加payable,而且要在回调里再次调用EtherStore的deposit(毕竟你改后的deposit是先加余额再转账,这是典型的重入高危写法); - 确保EtherStore合约有足够的ETH余额,能支撑多次转账;
- 你改后的
deposit逻辑其实是有重入漏洞的:先更新balances[msg.sender] += msg.value,再转ETH,攻击者可以在回调里重复调用deposit,让自己的余额被多次累加,实际却只转一次ETH,甚至薅合约已有余额的羊毛。
内容的提问来源于stack exchange,提问作者KimuraLeong
相关产品推荐
相关产品推荐

