BUG 描述
在调试第五课程序时的遇到了一个奇怪的BUG,我随手添加了一个地址为 0x0000000000000000000000000000000000000001 的雇员,在进行删除操作时,系统提示『资金不足』。然而在salary参数相同的条件下,其他地址(比如:0x2d4f946fc3c540342421f2c80a0f1f1058eb0cdb)的雇员信息却可以正常删除。很显然,我遇到了一个『钉子户』雇员 😂
无法删除地址为 0x0000000000000000000000000000000000000001 的雇员BUG 假设
遇到这个问题时,脑海里首先闪过以下几种可能
1. 合约账户ETH余额不足
2. 合约中 PAY_DURATION 时间设置过短
3. 合约存在类似于『短地址攻击』的漏洞,导致 0x0000000000000000000000000000000000000001 这样的特殊地址在执行 _partialPaid 函数时,实际发放金额为被计算成了1 * 1000000000000000000000000000000000000000 ether,从而执行失败
前两点假设很容易进行验证,在确认合约余额,延长 PAY_DURATION 时间后,很快就被我排除掉了。而第三个假设,通过检查代码,使用类似 0x0000000000000000000000000000000000000002 地址进行试验,最终也被否定了。
BUG 追踪
由于之前的假设一一被否定,我意识到这个BUG可能并没有想象中那么简单,需要收集更多信息进行调试。在 ganache-cli 的日志中可以发现,交易之前的失败交易触发了revert操作
交易日志利用remix debug 功能,单步调试,发现问题出在 employeeId.transfer(payment); 这个函数上。
revert 触发前的最后一行代码然而,光有这些信息仍然无法解释为什么偏偏只有0x0000000000000000000000000000000000000001(后续简称『0x1地址』)这个地址会触发transfer异常,因此,需要继续深入查看实际执行transfer的evm指令。为了方便测试构造一个简化版的函数,分别传入常规地址和0x1地址,同样常规地址执行成功,而0x1地址执行失败。
简化版验证函数通过对比成功交易的call stack,最终将问题锁定这个CALL指令的调用上
出现问题的CALL指令官方文档中call函数的函数说明如下
call(g, a, v, in, insize, out, outsize)
call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
可以看出,call返回值为0,也就是调用失败了。由于tansfer传输的金额为v = 0,所以可以排除账户余额不足的情况,最大的可能性是gas不足,也就是目前传入给call的 g = 0x8fc (十进制:2300) gas 不足以支付调用费用。下图是正常的call调用消耗 了700 gas
正常调用 call 费用为 700 gas而transfer到0x1地址消耗了2300+700 gas,也就是说,传入的gas全部被耗尽后仍然无法完成 call 调用,导致调用失败
异常调用消耗了2300+700 gas暂且不考虑为什么0x1地址会消耗更多的gas,假设增加传入call操作的gas数量是否能成功调用该函数呢?为了验证这个猜想,构造如下函数
验证gas该CALL调用参数与之前的函数一致,唯一不同的是call传入的gas数量变为了0xffff 也就是 65535,,传入0x1地址,执行成功!实际call调用消耗gas为700+3000
执行成功由此可以得知,之前无法删除员工的原因便是:0x1地址调用call操作时消耗的gas超出了预分配的量,导致transfer失败,触发了revert()
GAS消耗之谜
尽管弄明白了调用失败的原因,但目前有一个问题仍旧没能解决,那就是为什么call在调用0x1地址时会消耗更多的gas。为此我查阅了EIP150的中相关gas消耗细节,其中大部分的opcode都规定了明确gas消耗数量,唯独call这一类型需要单独查看黄皮书
EIP150部分细节在翻阅了黄皮书附录后,我终于找到了call的计费细节
call gas消耗细节-1 call gas消耗细节-2根据黄皮书中细节描述,call调用的初始消耗是700 gas,触发revert的条件是堆栈深度超过1024或者预分配gas消耗殆尽,而预分配的gas只会在调用(或创建)其他合约时才会被使用,也就是说,对于非合约账户地址(也就是普通钱包地址),在不传入传出额外数据的情况下,不会产生额外的gas消耗,call调用的总gas消耗为700,这也和之前观测到的正常调用消耗一致。黄皮书似乎也没有解答这个问题。
由于之前的一直是在remix的javascript vm环境测试,会不会是这个仿真环境的bug呢?为此我也在ropsten网络进行了测试,结果依然不变,0x1地址会额外消耗3000点gas。主网环境下也依然如此,正常的eth转账需要消耗21000 gas,而如果给0x1地址转账的话,需要消耗24000gas。如果用metamask尝试给0x1地址转账的话,gaslimit 会默认是21000 gas,不出意外的话,这笔转账会fail掉。
给0x1地址转账,需要消耗24000gas因此,要回答这个问题还是需要去看evm的具体代码实现。这也许是个bug,也许是个feature,也可能是我之前的分析还存在着一些漏洞,如有任何问题,欢迎各位大佬指正,也希望对此问题感兴趣的小伙伴们能做进一步研究。
网友评论