写好智能合约还真挺难的。
各种问题
这有什么新鲜的,编程本非易事。
首先,社会上引领技术潮流的,反倒是社媒上一撮精心佯装成程序员的营销高手们,于是乎引发了技术决策一边倒的倾向各种道听途说。然后就是以销售不负责软件为荣批发行业。
反正吧,写个靠谱的代码就那么苦难。
其次,学术上教授们似乎也放弃教授并发性了,改成教学生们“事件驱动”框架。“事件驱动框架”它就是“处理并发”的框架(就是kill并发),抓取互斥锁使代码无法利用并发性,进而避免产生并发错误。
反正吧,写个靠谱的代码就那么困难。
尽管以太坊智能合约不存在并发性这说,但也没躲开重入漏洞的威胁。且重入漏洞各种避之不及的坑,不信问问关注过DAO攻击的人。
那么,今天我们言简意赅的解释下这些坑能有多微妙。
下面是个token合约,31行代码,猛一看是个面向条件编程的榜样。
试试看,能否找出错误。

给个提示
首先,有没注意到此合约执行状态更改操作,根据先前的检查,在外部调用后预测正确性。
问题是:1、合约最后没有执行外部调用;2、也没有防止重入调用的互斥锁。
这个合约其实是个反例。
这个反模式正是DAO出问题的地方。
所以,提示#1是这种用法是个红旗。
其次,有没注意到代码中还检查了一个全局不变量,即合约余额(this.balance)不能低于合约认为的数量(totalSupply)。 这叫防守编程,本身没错。虽说模式有问题,但合约看起来是很安全的,因为不变量检查似乎是保护了合约的重要的假设。
但是注意一下函数入口的函数修饰符中执行不变量检查。 意思是说,这种特殊的模式中,本该始终保持的全局不变量,倒像是个后置条件。 所以,提示#2,不变量视为后置条件总是安全的?
最后,注意更改第7行时引入的漏洞
if (this.balance != totalSupply) throw;
改成
if (this.balance < totalSupply) throw;
因此,从检查一个强条件,变成检查一个弱条件。这是提示#3。
若合约中的实际余额高于合约认为的数额会发生什么? 条件弱化够避免了有人向合约发送一小笔付款时,余额与内部计算值不匹配引发的合约自行失效。 但是,但是,检查只说了余额大于totalSupply,要是余额小于totalSupply怎么办?
有这么个事儿,说这三个问题同时出现的话,允许合约持有更多的资金(多于合约认为应该持有的资金),后果就是攻击者能够凭空提现。
注意:合约存款函数也存在缺陷,因为它用的是用户指定的金额,而非msg.amount。 这个函数应该是内置的,且合约还应该有个能用msg.amount调用存款的默认函数。
黑了
详情可以参见:

大概其是说,攻击者先假装提现,让合约觉得余额少了。然后将提现的ETH重新存入并转移到第二个地址。 那么现在,合约中的余额超过了合约认为自己持有的数额,那么所有的超额部分都可以被提走。
“噫吁嚱”是我看到这场黑之后的反应。 重入攻击下,智能合约很容易变智障。一个31行代码的简单token合约,用防御模式编写,在函数入口孜孜不倦的检查不变量,还是上了重入攻击的套。
小贴士
要点就是:不要在合约中执行外部调用。 若调用了,那确保外部调后不再干别的。 若还想干别的,上互斥锁防范重入调用。 不只是刚外部调用的函数需要互斥锁,所有函数中都得上。
给工具链开发者的贴士:Solidity编译器或类似lint的工具需要对这些反模式进行检测并警告。
从更高的层面来说,我觉得EVM允许合约默认功能参与任意复杂行为没什么必要。 好比,除非A允许,否则A调用B时,EVM可以直接禁止合约B及其所有被调用方C到Z回调合约A。或者说,叫跨合约重入禁令,是默认的(可以解禁)。合约A可以不停的调用自己的内部函数,但若调用了外部函数,就回不来了。
这种禁止重入调用的禁令应该不会影响到合约功能,而且还能规避重入攻击。 不介意被重入调用的合约大可以更改默认。当然了,并不是说这种修复方法能规避所有外部调用引起的错误,好比还有跨合约错误等,但至少方向没错。默认禁令的方式,在合约的表达上、性能上或其他,并没有牺牲掉什么东西。那么不介意重入调用的合约,决定关闭默认,也是可以的。
默认禁令的方式或许有点麻烦。 我相信或许有更全面的方法可以避免这类错误。
若真有,请不吝赐教。
Reentrancy Woes in Smart Contractshackingdistributed.com

网友评论