智能合约可能出现的安全问题(三)
注意事项:
- 链上的数据是公开的
- 权衡Abstract合约和Interfaces
- 在双方或多方参与的智能合约中,参与者可能会“脱机离线”后不再返回
- 使Fallback函数尽量简单
- 明确标明函数和状态变量的可见性
- 将程序锁定到特定的编译器版本
- 小心除零 (Solidity < 0.4)
- 区分函数和事件
- 使用Solidity更新的构造器
注意事项
1.记住链上的数据是公开的
许多应用需要提交的数据是私有的,直到某个时间点才能工作。游戏(比如,链上游戏rock-paper-scissors石头剪刀布)和拍卖机(比如,sealed-bid second-price auctions)是两个典型的例子。如果你的应用存在隐私保护问题,一定要避免过早发布用户信息。
例如:
- 在游戏石头剪刀布中,需要参与游戏的双方提交他们“行动计划”的hash值,然后需要双方随后提交他们的行动计划;如果双方的“行动计划”和先前提交的hash值对不上则抛出异常。
- 在拍卖中,要求玩家在初始阶段提交其所出价格的哈希值(以及超过其出价的保证金),然后在第二阶段提交他们所出价格的资金。
- 当开发一个依赖随机数生成器的应用时,正确的顺序应当是(1)玩家提交行动计划,(2)生成随机数,(3)玩家支付。产生随机数是一个值得研究的领域;当前最优的解决方案包括比特币区块头,通过http://btcrelay.orghash-commit-reveal方案比如,一方产生number后,将其散列值提交作为对这个number的“提交”,然后在随后再暴露这个number本身和验证), RANDAO。
- 如果你正在实现频繁的批量拍卖,那么hash-commit机制也是个不错的选择。
2. 权衡Abstract合约和Interfaces
Interfaces和Abstract合约都是用来使智能合约能更好的被定制和重用。Interfaces是在Solidity 0.4.11中被引入的,和Abstract合约很像但是不能定义方法只能申明。Interfaces存在一些限制比如不能够访问storage或者从其他Interfaces那继承,通常这些使Abstract合约更实用。尽管如此,Interfaces在实现智能合约之前的设计智能合约阶段仍然有很大用处。另外,需要注意的是如果一个智能合约从另一个Abstract合约继承而来那么它必须实现所有Abstract合约内的申明的函数,否则它也会成为一个Abstract合约。
3. 在双方或多方参与的智能合约中,参与者可能会“脱机离线”后不再返回
不要让退款和索赔流程依赖于参与方执行的某个特定动作而没有其他途径来获取资金。比如,在石头剪刀布游戏中,一个常见的错误是在两个玩家提交他们的行动计划之前不要付钱。然而一个恶意玩家可以通过一直不提交它的行动计划来使对方蒙受损失 -- 事实上,如果玩家看到其他玩家泄露的行动计划然后发现自己输了,那么他完全有理由不再提交他自己的行动计划。这些问题也同样会出现在通道结算。
当这些情形出现导致问题后:
(1)提供一种规避非参与者和参与者的方式,可能通过设置时间限制
(2)考虑为参与者提供额外的经济激励,以便在他们应该这样做的所有情况下仍然提交信息。
4. 使Fallback函数尽量简单
每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的Fallback函数。
此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。
如果你希望能够监听.send()
或.transfer()
接收到Ether,则可以在fallback函数中使用event,让客户端监听相应事件做相应处理。谨慎编写fallback函数以免gas不够用。
下述提供给fallback函数可执行的操作会比常规的花费得多一点。
- 写入到存储(storage)
- 创建一个合约
- 执行一个外部(external)函数调用,会花费非常多的gas
- 发送ether
// bad
function() payable { balances[msg.sender] += msg.value; }
// good
function deposit() payable external { balances[msg.sender] += msg.value; }
function() payable { LogDepositReceived(msg.sender); }
5. 明确标明函数和状态变量的可见性
明确标明函数和状态变量的可见性。函数可以声明为 external,public, internal 或 private。 分清楚它们之间的差异, 例如external 可能已够用而不是使用 public。对于状态变量,external是不可能的。明确标注可见性将使得更容易避免关于谁可以调用该函数或访问变量的错误假设。
// bad
uint x; // the default is private for state variables, but it should be made explicit
function buy() { // the default is public
// public code
}
// good
uint private y;
function buy() external {
// only callable externally
}
function utility() public {
// callable externally, as well as internally: changing this code requires thinking about both cases.
}
function internalAction() internal {
// internal code
}
6. 将程序锁定到特定的编译器版本
智能合约应该应该使用和它们测试时使用最多的编译器相同的版本来部署。锁定编译器版本有助于确保合约不会被用于最新的可能还有bug未被发现的编译器去部署。智能合约也可能会由他人部署,而pragma标明了合约作者希望使用哪个版本的编译器来部署合约。这当然也会付出兼容性的代价。
// bad
pragma solidity ^0.4.4;
// good
pragma solidity 0.4.4;
7. 小心除零 (Solidity < 0.4)
早于0.4版本, 当一个数尝试除以零时,Solidity 返回zero并没有 throw
一个异常。确保你使用的Solidity版本至少为 0.4。
8. 区分函数和事件
为了防止函数和事件(Event)产生混淆,声明一个事件使用大写并加入前缀(建议LOG)。对于函数, 始终以小写字母开头,构造函数除外。
// bad
event Transfer() {}
function transfer() {}
// good
event LogTransfer() {}
function transfer() external {}
9. 使用Solidity更新的构造器
使用更合适的构造器/别名,如selfdestruct
而不是suicide
,使用keccak256
而不是sha3
。 像require(msg.sender.send(1 ether))
的模式也可以简化为使用transfer()
,如msg.sender.transfer(1 ether)
。
参考文章
上一篇: 级联操作