solidity智能合约的安全(二)

solidity智能合约的安全(二)_第1张图片

上次我们谈到了由于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或者其他代币



solidity智能合约的安全(二)_第2张图片

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


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

你可能感兴趣的:(solidity智能合约的安全(二))