solidity智能合约的安全(二)

上次我们谈到了由于solidity智能合约代码的公开性和行业现状,这一领域的安全状况令人堪忧,所幸目前这种情况正在被重视,有些组织和个人已经做了一些有意义的实践与总结。下面是solidity社区总结出的安全方面的主要关注点与应对思路。

智能合约的安全问题与最佳实践

1. 重入


如果一个智能合约A调用了另一个智能合约B,那么控制权将从A完全传递给B,也就是说,如果B再回调回A也,然后再调用B,然后再调用A...这样循环下去,A也是没有办法的。下面的合约将允许一个攻击者多次得到退款,因为它使用了 call ,默认发送所有剩余的 gas:

pragma solidity ^0.4.0;// THIS CONTRACT CONTAINS A BUG - DO NOT USE

contract Fund { 

 /// Mapping of ether shares of the contract. 

     mapping(address => uint) shares; /// Withdraw your share. 

     function withdraw() public { 

         if (msg.sender.call.value(shares[msg.sender])()) 

             shares[msg.sender] = 0; 

     }

}

作为改善措施,应该使用使用“检查-生效-交互”(Checks-Effects-Interactions)模式编写函数代码:

第一步,做检查工作,例如参数是否合法,发送者是否合法...,这些检查工作应该首先被完成。

第二步,状态变量修改。

第三步,与其他合约交互,这一步在任何合约中都应该最后被调用。

由于对已知合约的调用反过来也可能导致对未知合约的调用,所以最好是一直保持使用这个模式编写代码。按照这一模式,以上代码应该这么写:

pragma solidity ^0.4.11;

contract Fund { /// 合约中 |ether| 分成的映射。 

     mapping(address => uint) shares; /// 提取你的分成。 

     function withdraw() public { 

         var share = shares[msg.sender]; 

         shares[msg.sender] = 0; 

         msg.sender.transfer(share); 

     }

}

2.address.send(), address.transfer()  vs address.call.value()()


这三个调用都可以用来向一个智能合约转账,但仍然有以下区别:

address.send()和address.transfer()是重入安全(可以防止重入)的. 原因是,这两种调用只分配到了2300 gas,这一数量一般只够记录一两条log。所以,当被调用的地址是一个合约,而且实现了可以被外界执行的默认函数,那么这个默认函数仍然会被调用到。address.transfer(y)效果等同于require(address.send(y));.

address.call.value(y)()将会把所有的gas发送到合约地址上并执行默认函数. 所以这个默认函数将会有足够的gas执行任何操作,包括重新调用原合约的接口,因此是重入不安全(不能防止重入)的。

因此,在用这三个接口转账时,一定要根据实际情况把安全状况考虑清楚。一般来说,首选address.send()和 address.transfer()。其次选address.call.value(y)(),在使用address.call.value(y)()的时候最好限定所能使用的最多gas。

3.push模式 vs pull模式


在以上的讨论中,尽管你使用address.call.value(y)()的时候限定了所能使用的最多gas,仍然不意味着就没有问题了,因为也许一个你没有想到的漏洞所需要的gas本来就低于你的限定量。我们最好将调用限制到智能运行其本身的transaction,因此,在付款相关操作中,拉取模式(pull)应该优先于推送模式(push), 让我们看看以下例子:

// bad

contract auction { 

    address highestBidder; 

    uint highestBid; 

    function bid() payable { 

        require(msg.value >= highestBid); 

        if (highestBidder != 0) { 

            highestBidder.transfer(highestBid); // if this call consistently fails, no one else can bid

        } 

        highestBidder = msg.sender; 

        highestBid = msg.value; 

    }

}

以上例子中,退款采用的是push模式,如果有一个攻击者在fallback中调用require(0);然后参与以上拍卖之后,其他人再参与拍卖时以上代码highestBidder.transfer(highestBid);便会失败。从而达到锁定拍卖的目的。而如果退款时采取pull模式,则会避免这一缺陷:

// good

contract auction { 

     address highestBidder; 

     uint highestBid; 

     mapping(address => uint) refunds; 

     function bid() payable external { 

         require(msg.value >= highestBid); 

         if (highestBidder != 0) { 

             refunds[highestBidder] += highestBid; // record the refund that this user can claim 

         } 

         highestBidder = msg.sender; 

         highestBid = msg.value; 

     } 

     function withdrawRefund() external { 

         uint refund = refunds[msg.sender]; 

         refunds[msg.sender] = 0; 

         msg.sender.transfer(refund);

     }

}

4.合约间调用处理


Solidity 开放了一些底层调用函数如 address.call(),address.callcode(),address.delegatecall()和 andaddress.send(). 这些底层调用的一个特点是:他们调用失败时不会抛出异常,只是会返回失败,而异常在合约间的调用中是可以传递的,返回的失败结果却不能。因此,当调用这些底层函数时一定要考虑调用失败的情形,例如,我们不能这么写代码:

// bad

someAddress.send(55);

someAddress.call.value(55)(); 

someAddress.call.value(100)(bytes4(sha3("deposit()"))); 

而应该这么写:

// good

if(!someAddress.send(55)) { 

     // Some failure code

}

ExternalContract(someAddress).deposit.value(100);

5.不要假设智能合约里没有eth或者其他代币



攻击者甚至可以在你的合约没有创建之前向你的智能合约转入一笔eth。当然也有一些其他办法巧妙的向你的智能合约存入eth,例如:攻击者可以创建一个合约,向这个合约中转入少量eth,然后调用selfdestruct(victimAddress),这个victimAddress填成你的合约地址,这样eth就转到你的合约里了,这种方法没有人能阻止。


事实上,除了以上基本准则,人们还在不断总结经验并抽象出一些代码模式。相信在人们的共同努力下,solidity智能合约会越来越安全。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容