本文英文版原地址:http://solidity-cn.readthedoc...
由于本人英语能力有限,使用谷歌翻译,本篇好多地方不通顺。有能力的能够直接看英文版本。html
虽然构建按预期工做的软件一般很是容易,但要检查人们以不能预料到的方式使用它,要困可贵多。数组
在Solidity中,这更加剧要,由于您可使用智能合约来处理令牌(tokens)或可能更有价值的东西。此外,智能合约的每一次执行都在公开场合进行,除此以外,源代码一般是可用的。安全
固然,你须要考虑有多大的风险:你能够将智能合约与对公众开放的Web服务(以及对恶意行为者)以及甚至开放源代码进行比较。若是您只将该购物清单存储在该Web服务上,则可能没必要太在乎,但若是您使用该Web服务管理您的银行帐户,则应该更加当心。app
本节将列出一些陷阱和通常安全建议,但固然可能永远不会完整。另外,请记住,即便您的智能合约代码没有缺陷,编译器或平台自己也可能有错误。能够在已知错误列表中找到编译器的一些公开已知安全相关错误列表,这些错误也是机器可读的。请注意,有一个错误赏金程序涵盖了Solidity编译器的代码生成器。ide
与往常同样,使用开源文档,请帮助咱们扩展本节(特别是,一些示例不会受到伤害)!模块化
陷阱函数
私人信息和随机性
您在智能合约中使用的全部内容都是公开可见的,即便是标记为private
的本地变量和状态变量。区块链
若是你不但愿矿工可以做弊,在智能合同中使用随机数字是很是严峻的一件事。ui
重入(Re-Entrancy)开放源代码
合同(A)与另外一合同(B)的任何互动以及乙方的任何转让均将控制移交给该合同(B)。 这使得在这个交互完成以前B能够回调A. 举一个例子,下面的代码包含一个bug(它只是一个片断而不是一个完整的合同):
pragma solidity ^0.4.0;
//这个函数包含一个bug---不要使用 contract Fund { /// 合约的映射。 mapping(address => uint) shares; /// 撤回你的份额。 function withdraw() public { if (msg.sender.send(shares[msg.sender])) shares[msg.sender] = 0; } }
这里的问题不是太严重,由于做为send
的一部分,gas
是有限的,但它仍然暴露出一个弱点:以太(Ether)转移老是包含代码执行,因此接收方多是一个合约,能够调回撤回。 这将让它获得屡次退款,并基本上检索合同中的全部以太网。 特别是,如下合同将容许攻击者屡次withdraw
,由于它使用默认转发全部剩余gas
的call
:
pragma solidity ^0.4.0; // 这个函数包含一个bug---不要使用 contract Fund { mapping(address => uint) shares; function withdraw() public { if (msg.sender.call.value(shares[msg.sender])()) shares[msg.sender] = 0; } }
为了不从新入侵(Re-Entrancy),您可使用Checks-Effects-Interactions模式,以下所述:
pragma solidity ^0.4.11; contract Fund { mapping(address => uint) shares; function withdraw() public { var share = shares[msg.sender]; shares[msg.sender] = 0; msg.sender.transfer(share); } }
请注意,重入不只影响以太转移,还影响其余合同上的任何功能调用的。 此外,您还必须考虑多合同状况。 被called
的合同能够修改您依赖的另外一份合同的状态。
Gas限制和循环
没有固定迭代次数的循环(例如取决于存储值的循环)必须当心使用:因为区块中gas限制,交易只能消耗必定量的gas。 不管是明确的仍是仅仅因为正常的操做,循环中的迭代次数可能会超出区块中gas限制,这会致使整个合同在某个点停滞。 这可能不适用于仅用于从区块链读取数据的constant
函数。 尽管如此,这些功能可能会被其余合同做为链上操做的一部分进行调用,并将其拖延。 请在合同文件中明确说明这些状况。
发送和接收Ether
selfdestruct(x)
。addr.call.value(x)()
将更多gas转发给接收合同。 这与addr.transfer(x)
基本相同,只是它转发了全部剩余的gas并打开了接收方执行更昂贵的操做的能力(而且它只返回失败代码而且不会自动传播错误)。 这可能包括回拨发送合约或您可能没有想到的其余状态更改。 所以它为诚实用户提供了极大的灵活性,同时也为恶意行为者提供了很大的灵活性address.transfer
发送Ether,有一些细节须要注意:1.若是收件人是合同,它将致使其执行回退功能,从而能够回拨发送合同。
2.发送Ether可能会因呼叫深度超过1024而失败。因为caller彻底控制呼叫深度,所以可能会强制传送失败; 考虑这种可能性或使用发送,并确保始终检查其返回值。 更好的是,用收款人能够取消Ether的模式写下你的合同。
3.发送Ether也可能失败,由于收货合同的执行须要的gas超过了分配的数量(明确地经过使用要求,断言,还原,抛出或由于操做太昂贵) - 它“耗尽gas”(OOG)。 若是您使用转帐或发送返款金额支票,这可能为收件人提供阻止发送合同中进度的手段。 一样,这里的最佳作法是使用“撤回”模式而不是“发送”模式。
Callstack深度
外部函数调用可能会随时失败,由于它们超过了1024的最大调用堆栈。在这种状况下,Solidity会引起异常。 恶意行为者在与你的合同进行交互以前可能会强制调用堆栈的high value。
请注意,若是调用堆栈已耗尽,则.send()
不会引起异常,但在此状况下返回false。 低等级函数.call()
,.callcode()
和.delegatecall()
的行为方式相同。
tx.origin
切勿使用tx.origin
进行受权。 假设你有这样的钱包合约:
pragma solidity ^0.4.11; //这个函数包含一个bug---不要使用 contract TxUserWallet { address owner; function TxUserWallet() public { owner = msg.sender; } function transferTo(address dest, uint amount) public { require(tx.origin == owner); dest.transfer(amount); } }
如今有人欺骗你将ether发送到这个攻击钱包的地址:
pragma solidity ^0.4.11; interface TxUserWallet { function transferTo(address dest, uint amount) public; } contract TxAttackWallet { address owner; function TxAttackWallet() public { owner = msg.sender; } function() public { TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance); } }
若是您的钱包已经检查了msg.sender
的受权,它会获得攻击钱包的地址,而不是全部者地址。 但经过检查tx.origin
,它会获得启动交易的原始地址,该地址还是全部者地址。 攻击钱包当即消耗您的全部资金。
备注
for(var i = 0; i <arrayName.length; i++){...}
中,i的类型将是uint8,由于这是保存值0所需的最小类型。若是数组有255个元素以上,循环将不会终止。constant
关键字目前不禁编译器强制执行。此外,它不是由EVM强制执行的,因此“声称”保持不变的合约功能可能仍会致使状态发生变化。msg.data
(这构成了可塑性风险):您可使用原始字节参数为0xff000001
和0x00000001
调用函数f(uint8 x)
。这两种方法都是与合同相关联的,并且它们看起来都像x
相关的数字1
。可是msg.data
会有所不一样,因此若是您使用keccak256(msg.data)
作任何事情,您将获得不一样的结果。推荐作法
限制Ether的量。
限制能够存储在智能合约中的Ether(或其余tokens)数量。 若是您的源代码,编译器或平台有错误,这些资金可能会丢失。 若是你想限制你的损失,限制Ether的数量。
保持小型化和模块化
保持合同规模小,易于理解。 在其余合同或库中找出无关的功能。 关于源代码质量的通常建议固然适用:限制局部变量的数量,函数的长度等等。 记录你的功能,以便其余人能够看到你的意图是什么,以及它是否与代码不一样。
使用检查 - 效果 - 互动(Checks-Effects-Interactions )模式
大多数函数将首先执行一些检查(谁调用函数,是范围内的参数,他们是否发送了足够多的Ether,人员是否具备tokens等)。 这些检查应该先完成。
做为第二步,若是全部检查都经过了,则应该对当前合同的状态变量产生影响。 与其余合同的交互应该是任何功能的最后一步。
早期合同延迟了一些效果,并等待外部函数调用以非错误状态返回。 因为上述重入问题,这一般是一个严重的错误。
请注意,对已知合同的调用也可能致使对未知合同的调用,因此最好始终应用此模式。
包含故障安全模式
在使系统彻底分散化的同时将删除任何中介,这多是一个好主意,特别是对于新代码,可能包含某种故障安全机制:
您能够在智能合约中添加一个函数,执行一些自我检查,如“有任何Ether泄露?”,“tokens的总和是否等于合同的余额?” 或相似的东西。 请记住,你不能使用太多的gas,因此经过脱链(off-chain)计算可能须要帮助。
若是自检失败,合同会自动切换到某种“故障安全”模式,例如,禁用大部分功能,将控制权移交给固定和受信任的第三方,或者仅将合同转换为简单的“ 把个人钱还给我“合同。
形式化验证
使用形式验证,能够执行自动化的数学证实,证实源代码符合特定的正式规范。 规范仍然是正式的(就像源代码同样),但一般要简单得多。
请注意,形式验证自己只能帮助你理解你所作的事情(规范)和你如何作(实际实现)之间的差别。 您仍然须要检查规格是不是您想要的,而且您没有错过任何意想不到的效果。