安全注意事项
虽然通常很容易构建按预期工作的软件,但是更难检查没有人可以以预期的方式使用它。
在Solidity中,这更重要,因为您可以使用智能合同来处理令牌,或者甚至更有价值的东西。 此外,每次执行智能合同都会在公共场合发生,除此之外,源代码通常是可用的。
当然,你总是必须考虑到多少问题:您可以将智能合同与对公众开放的网络服务进行比较(因此也可以与恶意角色进行比较),甚至可以开源。 如果您仅将该杂货清单存储在该Web服务上,则可能不需要太多的关心,但如果您使用该Web服务管理您的银行帐户,则应该更加小心。
本节将列出一些陷阱和一般安全建议,但当然可以永远不会完成。 另外,请记住,即使您的智能合同代码是无错误的,编译器或平台本身也可能出现错误。 可以在已知错误列表中找到编译器的一些已知的安全相关错误的列表,它们也是机器可读的。 请注意,有一个错误赏金程序涵盖了Solidity编译器的代码生成器。
和往常一样,使用开源文档,请帮助我们扩展本节(特别是一些例子不会造成伤害)!
陷阱
私有信息和随机性
您在智能合同中使用的一切都是公开的,即使局部变量和状态变量标记private
。
如果您不希望矿工能够作弊,在智能合同中使用随机数是非常棘手的。
重入特性
任何来自合约(A)与另一份合约(B)的任何交互以及以太网转交给该合约的任何转让(B)。 这使得B在此交互完成之前可以回叫到A。 举一个例子,下面的代码包含一个错误(它只是一个代码段,而不是一个完整的合同):
pragma solidity ^0.4.0;
// 本合约包含错误 - 请勿使用
contract Fund {
/// 映射合约的ether
mapping(address => uint) shares;
/// 提取你的份额
function withdraw() {
if (msg.sender.send(shares[msg.sender]))
shares[msg.sender] = 0;
}
}
这里的问题不是太严重,因为gas作为发送的一部分,但仍然存在弱点:Ether传输总是包括代码执行,所以收件人可能是一个回叫撤回的合同。这将让它获得多次退款,并基本上检索合同中的所有Ether。
为了避免重入,您可以使用下面进一步列出的Checks-Effects-Interactions模式:
pragma solidity ^0.4.11;
contract Fund {
/// 映射合约的ether
mapping(address => uint) shares;
/// 提取你的份额
function withdraw() {
var share = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(share);
}
}
请注意,重入不仅仅是ether传输的影响,而是对另一个合约的任何功能调用。 此外,您还必须考虑多合约情况。 被调用合约可以修改您依赖的另一个合约的状态。
gas限制和循环
没有固定次数循环的循环,例如,依赖于存储值的循环,必须仔细使用:由于gas限制,事务只能消耗一定量的gas。 由于正常操作,明确地或仅仅是循环中的迭代次数可以超过块gas限制,这可能导致完整的合约在某一点停滞。 这可能不适用于只执行从块链接读取数据的常量函数。 尽管如此,这些功能可能被其他合约称为在线操作的一部分,并将其拖延。 请在您的合约文件中明确说明这些情况。
发送和接收Ether
合同和“外部账户”目前都无法阻止某人发送它们。 合同可以做出反应并拒绝常规传输,但是有一些方法可以在不创建消息调用的情况下移动Ether。 一种方法是简单地“挖掘”合同地址,第二种方式是使用selfdestruct(x)
。
如果合同收到Ether(没有被调用的功能),则执行回退功能。 如果没有回退功能,则Ether将被拒绝(通过抛出异常)。 在执行回退职能时,合约只能依靠当时可用的“gas津贴”(2300gas)。 这个补贴不足以以任何方式访问存储。 为确保您的合同能够以这种方式接收Ether,请检查回退功能的gas需求(例如Remix中的“详细信息”部分)。
有一种方法可以使用addr.call.value(x)()
将更多的气体转发给接收合同。 这与addr.transfer(x)
基本相同,只是它转发所有剩余的气体,并打开了收件人执行更昂贵的操作的能力(它只返回故障代码,不会自动传播错误)。 这可能包括回调发送合同或者您可能没有想到的其他状态更改。 因此,它可以为诚实用户提供极大的灵活性,也可以为恶意角色扮演角色。
如果要使用address.transfer
发送Ether,请注意一些细节:
1.如果收件人是合约,则会导致其回退功能被执行,从而可以回调发送合同。
2.由于呼叫深度超过1024,发送Ether可能会失败。由于呼叫者完全控制呼叫深度,因此可能会迫使传输失败; 考虑这种可能性或使用发送,并确保始终检查其返回值。 更好的是,使用收件人可以提取Ether的模式来编写合约。
3.发送以太网也可能失败,因为接收方合同的执行需要超过分配的气体量(明确地通过使用require
, assert
, revert
, throw
出或因为操作太贵) - 它“耗尽”(OOG)。 如果您使用transfer
或 send
返回值检查,这可能为收件人阻止发送合同中的进度提供了一种方法。 再次,这里的最佳做法是使用“撤回”模式而不是“发送”模式。
调用堆栈深度
外部函数调用可以随时失败,因为它们超过了1024的最大调用堆栈。在这种情况下,Solidity会引发异常。 在与您的合同交互之前,恶意代理人可能会强制调用堆栈达到高价值。
请注意,如果调用堆栈耗尽,则.send()
不会引发异常,而在这种情况下返回false。 低级函数.call()
,..callcode()
和.delegatecall()
的行为方式相同。
tx.origin
切勿使用tx.origin进行授权。 假设你有这样的钱包合同:
pragma solidity ^0.4.11;
// 本合约包含错误 - 请勿使用
contract TxUserWallet {
address owner;
function TxUserWallet() {
owner = msg.sender;
}
function transferTo(address dest, uint amount) {
require(tx.origin == owner);
dest.transfer(amount);
}
}
现在有人把你发送给这个攻击钱包的地址:
pragma solidity ^0.4.11;
interface TxUserWallet {
function transferTo(address dest, uint amount);
}
contract TxAttackWallet {
address owner;
function TxAttackWallet() {
owner = msg.sender;
}
function() {
TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
}
}
如果您的钱包已经检查了msg.sender的授权,它将获得攻击钱包的地址,而不是所有者的地址。 但是通过检查tx.origin,它会获得启动交易的原始地址,该地址仍然是所有者地址。 攻击钱包立即耗尽所有资金。
次要细节
对于for (var i = 0; i < arrayName.length; i++) { ... }
,i
的类型将为uint8,因为这是保存值0所需的最小类型。如果数组有更多 超过255个元素,循环将不会终止。
函数的常量关键字当前没有被编译器强制执行。 此外,它不是由EVM执行的,所以“claims”为常数的合同函数可能仍然导致状态的改变。
不占用32个字节的类型可能包含“脏高位”。 如果您访问msg.data,这是特别重要的 - 它带来可延展性风险:您可以制作一个调用函数f(uint8 x)的事务,原始字节参数为0xff000001和0x00000001。 两者都符合合同规定,两者将看起来像x一样,但是msg.data将不同,所以如果你使用keccak256(msg.data),你会得到不同的结果。
建议
限制Ether的量
限制可以存储在智能合同中的Ether(或其他令牌)数量。 如果您的源代码,编译器或平台有错误,这些资金可能会丢失。 如果你想限制你的损失,限制以太网的数量。
保持小而模块化
保持您的合约小而易于理解。 在其他合同或库中单独列出不相关的功能。 关于源代码质量的一般建议当然适用:限制局部变量的数量,函数的长度等。 记录您的功能,以便其他人可以看到您的意图,以及它是否与代码不同。
使用Checks-Effects-Interactions模式
大多数函数将首先执行一些检查(谁称为函数,是范围的参数,它们是否发送足够的以太网,该人是否具有令牌等)。 这些检查应该先做好。
作为第二步,如果所有支票通过,都应对当前合同的状态变量产生影响。 与其他合同的互动应该是任何功能的最后一步。
早期合同延迟了一些影响,并等待外部函数调用返回到非错误状态。 这通常是一个严重的错误,因为上面解释的重入问题。
请注意,对已知合同的调用也可能会导致未知合同的调用,因此总是应用此模式可能更好。
引入故障安全模式
在使系统完全分散化的同时,将删除任何中介,这可能是一个好主意,特别是对于新的代码,包括某种故障安全机制:
您可以在您的智能合同中添加一个功能,执行一些自检,例如“有任何ether泄漏?”,“令牌的总和等于合同的余额”或类似的事情。 请记住,您不能使用太多的气体,因此可能需要通过脱机计算来帮助。
如果自检失败,合同将自动切换为某种“故障安全”模式,例如,禁用大部分功能,对固定和受信任的第三方进行控制,或者将合同转换为简单的“ 给我回钱“合约。
形式化验证
使用形式验证,可以执行自动数学证明,您的源代码满足一定的正式规范。 规范仍然是正式的(就像源代码一样),但通常要简单得多。
请注意,正式验证本身只能帮助您了解您所做的(规格)与实际操作(实际执行)之间的区别。 您仍然需要检查规格是否符合您的要求,并且您不会错过任何意外的影响。