鸵鸟区块链

引介 | 与 EIP-1884 相关的安全考量

以太坊爱好者 2019-09-15 19:26 1.03W
摘要:

好像也没有多大问题……

背景

EIP 1884 已被纳入即将到来的以太坊 “伊斯坦布尔” 硬分叉中,它将:

  • 将 SLOAD 操作码的 Gas 消耗量从 200 提高到 800

  • 将 BALANCE 和 EXTCODEHASH 的 Gas 消耗量从 400 提高到 700

  • 加入一种新的操作码 SELFBALANCE,Gas 消耗量是 5

背后的理由是,因为状态数据大小的增长以及(相应的)从硬盘中取出状态树的额外读写开销,SLOAD、BALANCE 和 EXTCODEHASH 这几个操作码已经变得 “太过便宜” 了(对一个节点执行的实际工作量而言)。操作码的 Gas 消耗量与底层计算开销的严重不匹配,可能会导致多种问题、埋下网络攻击的种子(就像 2017 年的 “上海攻击” 那样)。

潜在问题

总的来说,给操作码重定 Gas 耗费,总是有可能会破坏那些明确依赖 “Gas 消耗量永远不变” 假设的合约。一直以来大家都认为操作码重定价是一个糟糕的办法,尤其是,一些主要的操作码 已经 在 Tangerine Whistle 中重新定价过了,那时候 SLOAD 的 Gas 耗费从 50 提高到了 200。不过,在某些情况下还可能出现更严重的问题:default 函数。

默认函数

所谓 default 函数,就是一种合约用来处理无数据调用的方法 —— 用来处理那些完全没有明确调用任何方法的 ETH 转账。它们一般会被来创建一个 event(使用 LOG 操作),然后外部系统就可以探测到这个事件,然后做出相应的操作(例如登记一笔交易已经完成)。一笔给合约的普通 ETH 转账总是会给接收者至少 2300 gas 作为 “津贴”。这笔 Gas 刚刚好可以让接收者能够发布一个事件,但又不足以让接收者能够更改状态(比如发起另一笔转账或者更新一个存储槽)。

EIP 1884 与默认函数

EIP-1884 可能导致的问题就是,用 2300 gas 调用 default 函数可能会失败,例如因为以下原因:

  • 钱包受限:合约仅在 balance(self) 低于一个确定的下限时才接受支付

  • 发送者被指定:合约仅接受来自一组预先许可的发送者的支付

  • 功能受限:合约仅在一个特定变量(也就是一个 slot)为真时才接受支付

现在,如果 default 函数在 2300 gas 的条件下停止工作,基本上不会有什么问题。例如,如果调用者是一个 EOA(外部所有者账户,也就是终端用户),调用者可以保证在一笔交易中附带比 21000 gas 多一点点的 gas。但别的地方可能出问题,例如:

  • 目标账户指定了发送者;

  • 发送者是智能合约,而且编程好了只用 transfer,不会附上额外的 gas

在这种情况下,从发送者到目标账户的 ether 会永久丢失、无法复原,除非有其它机制来处理这种情况(比如替换掉发送者)。

调查

我联系了 EthSecurity 社区来帮助研究这种情况。要点如下:

  • 没有 payable 默认函数的合约不会受影响

  • 当前用 2300 gas 无法运行其默认函数的合约不会受影响,例如,在默认函数里操作 SLOAD 或者转移 ether 的合约

Contract Library 分析

来自 Contract Library 的 Neville Grech,对部分反编译的主网合约做了静态分析。这一分析覆盖了 95% 的主网合约、测试网最近 50 万个块上的合约,40 万份各异的字节码,然后列出了那些可能被影响的合约。

  • 分析可以在此处获取,并且会自动更新。

注意,静态分析是一种无需执行程序便可分析所有程序行为的技术。该静态分析是根据下列部署在 contract-library.com 上的、简化的 datalog 技术规范来编写的。

% 约束那些从可能的路径到 fallback 函数的例外情况FallbackFunctionBlockEdge(from, to) :-   GlobalBlockEdge(from, to),   InFunction(from, f), FallbackFunction(f),   InFunction(to, g), FallbackFunction(g).% 使用常规的 gas 语义分析 fallback 函数路径% 取最短的路径GasCostAnalysis = new CostAnalysis(  Block_Gas, FallbackFunctionBlockEdge, 2300, min).% 用升级后的 gas 语义分析 fallback 函数路径% 取最短的路径EIP1884GasCostAnalysis = new CostAnalysis(  EIP1884Block_Gas, FallbackFunctionBlockEdge, 2300, min).FallbackWillFailAnyway(n - 2300) :-   GasCostAnalysis(*, n), n > 2300.% 使用额外的 n - m 单位的 gas 后,fallback 函数会失败EIP1884FallbackWillFail(n - m) :-   EIP1884GasCostAnalysis(block, n), n > 2300,   GasCostAnalysis(block, m),   !FallbackWillFailAnyway(*).
该分析测算了 fallback 函数中的所有可能路径的 Gas 消耗量,使用了 EIP-1884 部署前后的 Gas 设定。如果某个路径可以在旧的 gas 语义下完成,但无法在新的语义下完成,我们就抓出相应的合约。

该分析自动抓取出了主网上的 200 个合约,包括 Kyber Network 的合约和 CappedVault 合约。注意,如果 BALANCE 操作码的 gas 要求稍低一点(比如是 600),CappedVault 合约就还是能正常工作。该分析也发现了多个其它(带余额的)合约会在新的 gas 设定的多种条件下出错:EbcFund 合约中储存了超过 580 个 eth,在低于 2300 gas 的条件下将不再能接受捐献。

/**     * @dev fallback function to send ether to smart contract     **/    function () public payable {        require(currentStage == Stages.Started);        require(cfgMinDepositRequired <= msg.value && msg.value <= cfgMaxDepositRequired);if(donateList[msg.sender] == false) {if(transporter != address(0) && msg.sender == transporter) {//validate msg.dataif(msg.data.length > 0) {//init new game                    processDeposit(bytesToAddress(msg.data));                }else {emit Logger("Thank you for your contribution!.", msg.value);                }            }else {//init new game                processDeposit(msg.sender);            }        }else {emit Logger("Thank you for your contribution!", msg.value);        }    }
这份代码的最后一次调用是在 144 天以前。

NEXXO crowdsale 合约也是一样:

    modifier onlyICO() {require(now >= icoStartDate && now < icoEndDate, "CrowdSale is not running");        _;    }function () public payable onlyICO{require(!stopped, "CrowdSale is stopping");    }
NEXXO 会检查三个存储槽,icoStartDate、icoEndDate 以及 stopped,在新的 gas 规则下总共需要 2400 gas。

Crowd Machine Compute Token crowdsale 合约也是一样的问题:

  modifier onlyIfRunning  {require(running);    _;  }function () public onlyIfRunning payable {require(isApproved(msg.sender));    LogEthReceived(msg.sender, msg.value);  }
重要提醒:上述的 crowdsales 合约并没有从根本上被破坏,只是调用者需要添加超过 2300 gas 来参与该 ICO 合约。

Chain Security 分析

来自 ChainSecurity 的 Hubert Ritzdorf 对近期的交易执行了分析。该分析基于主网上发生的实际交易,然后观察哪些交易会在 SLOAD 操作码 Gas 耗费了提升到 800 的时候失败。部分结果在此处 可见。要点已在此处列明,附带下述评论:

前两种情况发生得更为频繁,其它的则不怎么常见。我们列出了最后一项,虽然在 EIP1884 实施后它仍能工作,但我们不确定这么 “深” 的交易的 gas 值在当前是如何确定的。我们希望引起大家对潜在问题的警惕。

Kyber Network

function() public payable {require(reserveType[msg.sender] != ReserveType.NONE);        EtherReceival(msg.sender, msg.value);    }
  • KyberNetwork 符合了这里所列的多个条件

  • 合约 “指定了发送者”

  • 主要通过其它合约来调用,这些调用依赖于 transfer 函数(限制在了 2300 gas)

我们联系了 KyberNetwork,虽然免不了有些繁琐的工作要做,但问题是可以解决的

技术上来说,做市商只需要部署新的储备合约

CappedVault

function total() public view returns(uint) {return getBalance() + withdrawn;    }function () public payable {require(total() + msg.value <= limit);    }
在这个合约中,withdrawn 是一个存储槽, limit 也是。
  • CappedVault 存有超过 4000 个 ether 和 7 万笔内部交易,同样符合下列条件:

  • 功能受限模式

    • 使用了两次 SLOAD 和一次 BALANCE

实现细节:

  • 该合约被编写成,只要发送给该合约的 ether 总数超过 33333,合约就 “断开”。也就是说,不管合约中当前有多少 ether,只要发送给合约的 ether 总数超过 3 万 3 千,就不再接受 ether。

    • 这就意味着已经有机制来处理默认函数停止运行的情况了。

  • limit 是一个存储 slot,但也可以实现为编译时常量(compile-time constant),从而省下一个 SLOAD

  • balance(self) 在伊斯坦布尔分叉后可重写为 SELFBALANCE

所以本质上,当前的用量是:

200 (sload limit) +200 (sload withdrawn) +400 (balance) = 800 gas

而在 EIP-1884 部署后:

5 (selfbalance) + 800 (sload withdrawn) = 805 gas

(完)

(文内提供了许多超链接,请点击阅读原文到 EthFans 网站上获取)


原文链接:

https://github.com/holiman/eip-1884-security

作者: holiman

翻译: 阿剑



声明: 鸵鸟区块链所有发布内容均为原创或授权发布,如需转载,请务必注明文章作者以及来源:鸵鸟区块链(微信公众号:MyTuoniao),任何不尊重原创的行为鸵鸟区块链都将进行责任追究!鸵鸟区块链报道和发布内容,不构成任何投资建议。

以太坊爱好者

以太坊爱好者

54 篇 作品
19.7W 总阅读量