以太坊智能合约的安全注意事项

匿名 (未验证) 提交于 2019-12-02 22:56:40

安全注意事项

虽然它通常是很容易建立一个运行正常的软件,它是更难检查,没有人能在这是一个方式来使用它没有预料到的。

在Solidity中,这更为重要,因为您可以使用智能合约来处理令牌,或者甚至可能使用更有价值的东西。此外,智能合约的每次执行都是在公共场合进行的,除此之外,源代码通常也是可用的。

当然,你总是要考虑有多大危险:你可以将智能合约与对公众开放的网络服务(以及对恶意行为者)甚至是开源的网络服务进行比较。如果您只将购物清单存储在该网络服务上,则可能不必太在意,但如果您使用该网络服务管理您的银行帐户,则应该更加小心。

本节将列出一些陷阱和一般安全建议,但当然可以永远不完整。另外,请记住,即使您的智能合约代码没有错误,编译器或平台本身也可能存在错误。可以在已知错误列表中找到编译器的一些公知的安全相关错误的 列表,这些错误也是机器可读的。请注意,有一个bug bounty程序,它涵盖了Solidity编译器的代码生成器。

与往常一样,使用开源文档,请帮助我们扩展此部分(特别是,一些示例不会受到伤害)!

陷阱

私人信息和随机性

您在智能合约中使用的所有内容都是公开可见的,甚至是标记的局部变量和状态变量private。
如果你不希望矿工能够作弊,在智能合约中使用随机数是非常棘手的。

重入

合约(A)与另一合约(B)之间的任何互动以及任何以太方移交控制权转移到该合约(B)。这使得B可以在此交互完成之前回调到A。举个例子,下面的代码包含一个bug(它只是一个片段,而不是一个完整的契约):

pragma solidity >=0.4.0 <0.6.0;  //本合约包含一个BUG - 请勿使用 contract Fund {     ///映射合约的以太份额。     mapping(address => uint) shares;     ///撤回你的份额。     function withdraw() public {         if (msg.sender.send(shares[msg.sender]))             shares[msg.sender] = 0;     } } 

这里的问题并不严重,因为天然气有限send,但它仍然存在一个缺点:以太网转移总是包括代码执行,所以收件人可能是一个回调的合约withdraw。这将让它获得多次退款并基本上检索合约中的所有以太币。特别是,以下合约将允许攻击者在使用时多次退款call,默认情况下转发所有剩余气体:

pragma solidity >=0.4.0 <0.6.0;  //本合约包含一个BUG - 请勿使用 contract Fund {     ///映射合约的以太份额。     mapping(address => uint) shares;     ///撤回你的份额。     function withdraw() public {         (bool success,) = msg.sender.call.value(shares[msg.sender])("");         if (success)             shares[msg.sender] = 0;     } } 

为避免重新入侵,您可以使用Checks-Effects-Interactions模式,如下所述:

pragma solidity >=0.4.11 <0.6.0;  contract Fund {     ///映射合约的以太份额。     mapping(address => uint) shares;     ///撤回你的份额。     function withdraw() public {         uint share = shares[msg.sender];         shares[msg.sender] = 0;         msg.sender.transfer(share);     } } 

请注意,重新入侵不仅是以太传输的影响,而且是对另一个合约的任何函数调用的影响。此外,您还必须考虑多合约情况。被叫合约可以修改您依赖的另一个合约的状态。

气体限制和循环

没有固定迭代次数的循环,例如,依赖于存储值的循环,必须小心使用:由于块气限制,交易只能消耗一定量的气体。无论是明确地还是仅仅由于正常操作,循环中的迭代次数可以超过块气体限制,这可能导致完整的合约在某一点停滞。这可能不适用于view仅执行从区块链读取数据的函数。但是,这些功能可能被其他合约称为链上操作的一部分,并使这些功能失效。请在合约文件中明确说明此类情况。

发送和接收以太




  1. 如果收件人是合约,则会导致其执行回退功能,从而可以回调发送合约。
  2. 由于调用深度超过1024,发送以太网可能会失败。由于调用者完全控制调用深度,因此可以强制转移失败; 考虑到这种可能性或使用send并确保始终检查其返回值。更好的是,使用一种模式来编写合约,其中收件人可以撤销以太。
  3. 发送醚也可能会失败,因为收件人合约的执行需要比气体的分配金额(显式地使用require, assert,revert,throw或因为操作太昂贵) -它“耗尽气”(OOG)。如果您使用transfer或send使用返回值检查,这可能会为收件人提供阻止发送合约进度的方法。同样,这里的最佳实践是使用“撤销”模式而不是“发送”模式。

调用堆栈深度

外部函数调用可能会随时失败,因为它们超过1024的最大调用堆栈。在这种情况下,Solidity会抛出异常。恶意actor可能会在与您的合约交互之前强制调用堆栈达到较高值。
请注意,如果调用堆栈耗尽,.send()则不会抛出异常,而是false在这种情况下返回。低级别的功能 .call(),.callcode(),.delegatecall()和.staticcall()行为以同样的方式。

tx.origin

切勿使用tx.origin进行授权。假设您有这样的钱包合约:

pragma solidity >0.4.99 <0.6.0;  //本合约包含一个BUG - 请勿使用 contract TxUserWallet {     address owner;      constructor() public {         owner = msg.sender;     }      function transferTo(address payable dest, uint amount) public {         require(tx.origin == owner);         dest.transfer(amount);     } } 

现在有人欺骗你把以太送到这个攻击钱包的地址:

pragma solidity >0.4.99 <0.6.0;  interface TxUserWallet {     function transferTo(address payable dest, uint amount) external; }  contract TxAttackWallet {     address payable owner;      constructor() public {         owner = msg.sender;     }      function() external {         TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);     } } 

如果您的钱包已经检查msg.sender了授权,它将获得攻击钱包的地址,而不是所有者地址。但通过检查tx.origin,它获得了启动交易的原始地址,这仍然是所有者地址。攻击钱包立即消耗所有资金。

两个补码/下溢/溢出

与许多编程语言一样,Solidity的整数类型实际上不是整数。当值很小时,它们类似于整数,但如果数字较大则表现不同。例如,以下情况属实:。这种情况称为溢出。当执行操作时需要使用固定大小的变量来存储超出变量数据类型范围的数字(或数据)。一个下溢是相反的情况:。uint8(255) + uint8(1) == 0uint8(0) - uint8(1) == 255
一般来说,请阅读有关二进制补码表示的限制,即使对于有符号数也有一些更特殊的边缘情况。
尝试使用require将输入的大小限制在合理的范围内,并使用 SMT检查器查找潜在的溢出,或使用类似SafeMath的库,如果你想让所有溢出导致恢复。

次要细节

建议

严肃对待

如果编译器警告你某事,你应该更好地改变它。即使您不认为此特定警告具有安全隐患,也可能存在其他问题。我们发出的任何编译器警告都可以通过稍微更改代码来静音。
始终使用最新版本的编译器来通知所有最近引入的警告。

限制以太的数量

限制可以存储在智能合约中的以太(或其他令牌)的数量。如果您的源代码,编译器或平台有错误,这些资金可能会丢失。如果要限制损失,请限制以太网的数量。

保持小而模块化

保持合约规模小,易于理解。在其他合约或库中单独列出不相关的功能。关于源代码质量的一般建议当然适用:限制局部变量的数量,函数的长度等。记录您的功能,以便其他人可以看到您的意图是什么,以及它是否与代码的不同。

使用Checks-Effects-Interactions模式

大多数函数将首先执行一些检查(谁调用函数,范围内的参数,是否发送足够的以太,此人是否有令牌等)。应首先进行这些检查。

第二步,如果所有检查都通过,则应对当前合约的状态变量产生影响。与其他合约的互动应该是任何职能的最后一步。

早期合约延迟了一些影响并等待外部函数调用以非错误状态返回。由于上面解释的重入问题,这通常是一个严重的错误。

另请注意,对已知合约的调用可能会导致调用未知合约,因此最好始终应用此模式。

包含故障安全模式

虽然使系统完全分散将删除任何中介,但特别是对于新代码,包含某种故障安全机制可能是个好主意:

您可以在智能合约中添加一项功能,执行一些自我检查,例如“有任何以太软件漏出来吗?”,“令牌的总和是否等于合约的余额?”或类似的事情。请记住,您不能使用过多的气体,因此可能需要通过离线计算提供帮助。

如果自检失败,合约会自动切换到某种“故障保护”模式,例如,禁用大多数功能,将控制交给固定和可信任的第三方,或者只是将合约转换为简单的“把我的钱还给我“合约。

请求同行评审

检查一段代码的人越多,发现的问题就越多。要求人们审查您的代码也有助于进行交叉检查,以确定您的代码是否易于理解 - 这是良好智能合约的一个非常重要的标准。

形式验证

使用形式验证,可以执行自动数学证明,证明源代码符合某种正式规范。规范仍然是正式的(就像源代码一样),但通常要简单得多。

请注意,形式验证本身只能帮助您了解您所做的事情(规范)与实现方式(实际实现)之间的区别。您仍然需要检查规格是否符合您的要求,并且您没有错过它的任何意外影响。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!