安全注意事项
虽然它通常是很容易建立一个运行正常的软件,它是更难检查,没有人能在这是一个方式来使用它没有预料到的。
在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仅执行从区块链读取数据的函数。但是,这些功能可能被其他合约称为链上操作的一部分,并使这些功能失效。请在合约文件中明确说明此类情况。
发送和接收以太
• 合约和“外部账户”目前都无法防止有人向他们发送以太网。合约可以响应并拒绝常规转移,但有一些方法可以在不创建消息调用的情况下移动以太网。一种方法是简单地“挖掘”合约地址,第二种方式是使用selfdestruct(x)。
• 如果合约收到Ether(没有调用函数),则执行回退功能。如果它没有回退功能,则将拒绝以太(通过抛出异常)。在执行备用功能期间,合约只能依赖于当时可用的“气体津贴”(2300气体)。这笔津贴不足以修改存储(不要认为这是理所当然的,因为未来的硬叉可能会改变津贴)。为了确保您的合约能够以这种方式接收以太网,请检查回退功能的气体要求(例如,在Remix的“详细信息”部分中)。
• 有一种方法可以使用更多的天然气转发给接收合约 addr.call.value(x)("")。这基本上是相同的addr.transfer(x),只是它转发所有剩余的气体并开启接收者执行更昂贵的动作的能力(并且它返回失败代码而不是自动传播错误)。这可能包括回调发送合约或您可能没有想到的其他状态更改。因此,它为诚实用户提供了极大的灵活性。
• 如果您想使用以太网发送address.transfer,请注意以下某些细节:
调用堆栈深度
外部函数调用可能会随时失败,因为它们超过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的库,如果你想让所有溢出导致恢复。
次要细节
• 不占用整个32字节的类型可能包含“脏的高阶位”。如果您访问这一点尤其重要msg.data-它带来了延展性的风险:您可以精心创建调用函数交易与原始字节的说法,并与。两者都送入合约,都将像数尽可能而言,但会有所不同,因此,如果您使用的任何东西,你会得到不同的结果。f(uint8 x)0xff0000010x000000011xmsg.datakeccak256(msg.data)
建议
严肃对待
如果编译器警告你某事,你应该更好地改变它。即使您不认为此特定警告具有安全隐患,也可能存在其他问题。我们发出的任何编译器警告都可以通过稍微更改代码来静音。
始终使用最新版本的编译器来通知所有最近引入的警告。
限制以太的数量
限制可以存储在智能合约中的以太(或其他令牌)的数量。如果您的源代码,编译器或平台有错误,这些资金可能会丢失。如果要限制损失,请限制以太网的数量。
保持小而模块化
保持合约规模小,易于理解。在其他合约或库中单独列出不相关的功能。关于源代码质量的一般建议当然适用:限制局部变量的数量,函数的长度等。记录您的功能,以便其他人可以看到您的意图是什么,以及它是否与代码的不同。
使用Checks-Effects-Interactions模式
大多数函数将首先执行一些检查(谁调用函数,范围内的参数,是否发送足够的以太,此人是否有令牌等)。应首先进行这些检查。
第二步,如果所有检查都通过,则应对当前合约的状态变量产生影响。与其他合约的互动应该是任何职能的最后一步。
早期合约延迟了一些影响并等待外部函数调用以非错误状态返回。由于上面解释的重入问题,这通常是一个严重的错误。
另请注意,对已知合约的调用可能会导致调用未知合约,因此最好始终应用此模式。
包含故障安全模式
虽然使系统完全分散将删除任何中介,但特别是对于新代码,包含某种故障安全机制可能是个好主意:
您可以在智能合约中添加一项功能,执行一些自我检查,例如“有任何以太软件漏出来吗?”,“令牌的总和是否等于合约的余额?”或类似的事情。请记住,您不能使用过多的气体,因此可能需要通过离线计算提供帮助。
如果自检失败,合约会自动切换到某种“故障保护”模式,例如,禁用大多数功能,将控制交给固定和可信任的第三方,或者只是将合约转换为简单的“把我的钱还给我“合约。
请求同行评审
检查一段代码的人越多,发现的问题就越多。要求人们审查您的代码也有助于进行交叉检查,以确定您的代码是否易于理解 - 这是良好智能合约的一个非常重要的标准。
形式验证
使用形式验证,可以执行自动数学证明,证明源代码符合某种正式规范。规范仍然是正式的(就像源代码一样),但通常要简单得多。
请注意,形式验证本身只能帮助您了解您所做的事情(规范)与实现方式(实际实现)之间的区别。您仍然需要检查规格是否符合您的要求,并且您没有错过它的任何意外影响。