新闻开发人员企业区块链解释事件和会议新闻时事通讯
订阅我们的新闻.
电子邮件地址
我们尊重您的隐私
主页博客区块链开发
智能合约安全的Solidity最佳做法
从监控到时间戳记考虑,这里有一些专业提示,以确保您的以太坊智能合约得到加强。由ConsenSys 2020年8月21日发布于2020年8月21日
由我们的区块链安全专家团队ConsenSys Diligence提供.
如果您已经牢牢掌握了智能合约安全性的心态,并且已经掌握了EVM的特性,那么现在该考虑一些特定于Solidity编程语言的安全性模式了。在本综述中,我们将重点介绍针对Solidity的安全开发建议,这些建议也可能对开发其他语言的智能合约具有指导意义.
好吧,让我们进入.
正确使用assert(),require(),revert()
便利功能 断言 和 要求 可用于检查条件并在不满足条件的情况下引发异常.
这 断言 函数应仅用于测试内部错误和检查不变性.
这 要求 函数应用于确保满足输入或合同状态变量之类的有效条件,或用于验证从调用外部合同获得的返回值.
遵循此范例,形式分析工具可以验证永远无法达到无效的操作码:这意味着不会违反代码中的不变式且代码已被正式验证.
语用强度^ 0.5.0;合同共享者{函数sendHalf(地址应付款地址)公共应付款收益(单位余额){require(msg.value%2 == 0, "要求均值."); // Require()可以具有可选的消息字符串uint balanceBeforeTransfer = address(this).balance; (bool成功,)= addr.call.value(msg.value / 2)("");要求(成功); //由于如果转帐失败,我们将还原,//应该没有办法让我们仍然有一半的钱。 assert(address(this).balance == balanceBeforeTransfer-msg.value / 2); //用于内部错误检查的返回地址(this).balance; }}代码语言:JavaScript(javascript)
仅将修饰符用于检查
修饰符内部的代码通常在函数主体之前执行,因此任何状态更改或外部调用都将违反 检查效果互动 图案。此外,由于修饰符的代码可能与函数声明相距甚远,因此开发人员可能也不会注意到这些语句。例如,修饰符中的外部调用可能导致重新进入攻击:
合同注册处{地址所有者;函数isVoter(address _addr)外部收益(布尔){//代码}}合同选择{Registry Registry;修饰符isEligible(address _addr){require(registry.isVoter(_addr)); _; }函数vote()isEligible(msg.sender)public {//代码}}代码语言:JavaScript(javascript)
在这种情况下,注册管理机构可以通过在isVoter()内部调用Election.vote()进行再入攻击。.
笔记: 使用 修饰符 替换多个函数(例如isOwner())中的重复条件检查,否则在函数内部使用要求或还原。这使您的智能合约代码更具可读性,更易于审核.
当心整数除法舍入
所有整数除法均四舍五入为最接近的整数。如果需要更高的精度,请考虑使用乘数,或同时存储分子和分母.
(将来,Solidity将有一个 固定点 类型,这样会更容易。)
//错误的uint x = 5/2; //结果为2,所有整数除法均向下舍入到最接近的整数代码语言:JavaScript(javascript)
使用乘数可防止舍入,将来在使用x时需要考虑此乘数:
//好的uint乘数= 10; uint x =(5 *乘数)/ 2;代码语言:JavaScript(javascript)
存储分子和分母意味着您可以计算链下分子/分母的结果:
//好的uint分子= 5; uint分母= 2;代码语言:JavaScript(javascript)
注意之间的权衡 抽象合约 和 介面
接口和抽象合同都为智能合同提供了一种可自定义和可重复使用的方法。在Solidity 0.4.11中引入的接口类似于抽象协定,但不能实现任何功能。接口也有局限性,例如无法访问存储或无法从其他接口继承,这通常使抽象协定更加实用。虽然,接口对于在实施之前设计合同肯定是有用的。此外,请务必牢记,如果合同从抽象合同继承,则它必须通过覆盖实现所有未实现的功能,否则它也将是抽象的.
后备功能
使后备功能保持简单
后备功能 当发送没有参数的消息时(或没有函数匹配时)调用合约,当从.send()或.transfer()调用时只能访问2,300个gas。如果您希望能够从.send()或.transfer()接收以太币,则在后备功能中可以做的最大事情就是记录一个事件。如果需要计算更多的气体,请使用适当的函数.
//错误的function()应付帐款{余额[msg.sender] + = msg.value; } //良好的功能deposit()payable external {余额[msg.sender] + = msg.value; } function()payable {require(msg.data.length == 0);发出LogDepositReceived(msg.sender); }代码语言:JavaScript(javascript)
检查后备功能中的数据长度
自从 后备功能 不仅用于纯醚传输(没有数据),而且在没有其他功能匹配时也要调用,如果回退功能仅用于记录接收到的以太币,则应检查数据是否为空。否则,调用者将不会注意到您的合同使用不正确,并且调用了不存在的函数.
//错误的function()应付帐款{发出LogDepositReceived(msg.sender); } //好的function()应付帐款{require(msg.data.length == 0);发出LogDepositReceived(msg.sender); }代码语言:JavaScript(javascript)
明确标记应付款功能和状态变量
从Solidity 0.4.0开始,每个接收以太币的功能都必须使用payable修饰符,否则如果交易具有msg.value > 0将还原(除非被强迫).
笔记: 可能不明显的是:应付账款修饰符仅适用于来自外部合同的通话。如果我在同一合同的应付款功能中调用了不付款功能,则即使设置了msg.value,该不付款功能也不会失败.
明确标记功能和状态变量中的可见性
明确标记功能和状态变量的可见性。可以将功能指定为外部,公共,内部或私有。请了解它们之间的区别,例如,外部可能足以代替公共。对于状态变量,无法使用外部变量。明确标记可见性将使您更容易捕捉关于谁可以调用函数或访问变量的错误假设.
- 外部功能是合同接口的一部分。外部函数f不能在内部调用(即f()不起作用,但this.f()有效)。当外部函数接收大量数据时,它们有时会更高效.
- 公共功能是合同接口的一部分,可以在内部调用或通过消息调用。对于公共状态变量,将生成自动获取方法(请参见下文).
- 内部函数和状态变量只能在内部访问,而无需使用this.
- 私有功能和状态变量仅对它们在定义的合同中可见,而在派生合同中不可见. 笔记:合约内的所有内容对于区块链外部的所有观察者都是可见的,甚至是私有变量.
//错误的uint x; //默认值是内部的状态变量,但应将其设为显式函数buy(){//默认值是public // public code} //好的uint private y; function buy()外部{//仅可外部调用或使用this.buy()} function utility()public {//可外部和内部调用:更改此代码需要考虑两种情况。 }函数internalAction()内部{//内部代码}代码语言:PHP(php)
将编译指示锁定到特定的编译器版本
合同应使用经过最多测试的相同编译器版本和标志进行部署。锁定实用程序有助于确保不会使用例如最新的编译器意外地部署合同,而最新的编译器可能会更容易发现未发现的错误。合同也可能由其他人部署,并且杂注指示原始作者打算使用的编译器版本.
//错误的编译指示实效性^ 0.4.4; //良好的实用性0.4.4;代码语言:JavaScript(javascript)
注意:浮动的编译指示版本(即^ 0.4.25)可以在0.4.26-nightly.2018.9.25上正常编译,但是夜间版本不应用于编译生产代码.
警告: 当合同打算供其他开发人员使用时,可以允许Pragma语句浮动,例如库或EthPM包中的合同。否则,开发人员将需要手动更新编译指示以便本地编译.
看 SWC-103
使用事件来监视合同活动
部署合同后,有一种方法可以监视合同的活动,这很有用。实现此目的的一种方法是查看合同的所有交易,但是由于合同之间的消息调用未记录在区块链中,因此这可能是不够的。此外,它仅显示输入参数,而不显示对状态的实际更改。事件也可以用于触发用户界面中的功能.
合同慈善{> uint)余额;函数donate()应付帐款公共{余额[msg.sender] + = msg.value; }}合约游戏{函数buyCoins()应付公众{{5%捐给慈善机构charity.donate.value(msg.value / 20)(); }}代码语言:JavaScript(javascript)
在这里,游戏合约将对Charity.donate()进行内部调用。该交易不会显示在“慈善机构”的外部交易列表中,而只会在内部交易中显示.
事件是记录合同中发生的事情的便捷方法。发出的事件与其他合同数据一起保留在区块链中,可供以后审核。这是对上面示例的改进,它使用事件来提供慈善机构的捐赠历史.
合同慈善{映射(地址=> uint)余额;函数donate()应付帐款公共{余额[msg.sender] + = msg.value; //发出事件发出LogDonate(msg.value); }}合约游戏{函数buyCoins()应付公众{{5%捐给慈善机构charity.donate.value(msg.value / 20)(); }}代码语言:JavaScript(javascript)
在这里,通过慈善合同进行的所有交易(无论是否直接进行)都将显示在该合同的事件列表中以及捐赠的金额.
注意:首选较新的Solidity构造. 最好使用诸如selfdestruct(自杀)和keccak256(sha3以上)之类的构造/别名。像require(msg.sender.send(1 ether))这样的模式也可以简化为使用transfer(),就像msg.sender.transfer(1 ether)一样。退房 固体变化日志 进行更多类似的更改.
请注意,“内置”可能会被遮盖
目前有可能 阴影 Solidity中内置的全局变量。这允许合同覆盖内置功能,例如msg和revert()。虽然这个 旨在, 可能误导合同用户的合同真实行为.
合同PretendingToRevert {函数revert()内部常量{}}合同ExampleContract是PretendingToRevert {函数somethingBad()public {revert(); }}
合同用户(和审核员)应了解他们打算使用的任何应用程序的完整智能合同源代码.
避免使用tx.origin
切勿使用tx.origin进行授权,另一个合同可以使用一种方法来调用您的合同(例如,用户拥有一些资金),并且您的合同将授权该交易,因为您的地址在tx.origin中.
合同MyContract {地址所有者;函数MyContract()public {owner = msg.sender; }函数sendTo(地址接收者,单位数量)public {require(tx.origin == owner); (bool成功,)= receiver.call.value(amount)("");要求(成功); }}合同AttackingContract {MyContract myContract;地址攻击者; function AttackingContract(address myContractAddress)public {myContract = MyContract(myContractAddress);攻击者= msg.sender; } function()public {myContract.sendTo(attacker,msg.sender.balance); }}代码语言:JavaScript(javascript)
您应使用msg.sender进行授权(如果另一个合同调用您的合同,msg.sender将是合同的地址,而不是调用该合同的用户的地址).
你可以在这里读更多关于它的内容: 实体文档
警告: 除了授权问题外,将来还有可能从以太坊协议中删除tx.origin,因此使用tx.origin的代码将与以后的版本不兼容 Vitalik:“不要以为tx.origin会继续可用或有意义。”
还值得一提的是,使用tx.origin限制了合同之间的互操作性,因为使用tx.origin的合同不能被其他合同使用,因为合同不能是tx.origin.
看 SWC-115
时间戳依赖性
使用时间戳执行合同中的关键功能时,有三个主要注意事项,尤其是当操作涉及资金转移时.
时间戳操纵
请注意,矿工可以操纵该区块的时间戳。考虑一下 合同:
uint256常量私有盐= block.timestamp;函数random(uint Max)常量私有返回值(uint256结果){//获得最佳随机性种子uint256 x =盐* 100 / Max; uint256 y =盐*块数/(盐%5); uint256种子= block.number / 3 +(盐%300)+ Last_Payout + y; uint256 h = uint256(block.blockhash(seed));返回uint256((h / x))%Max +1; //介于1到最大之间的随机数}代码语言:PHP(php)
当合同使用时间戳为随机数播种时,矿工实际上可以在经过验证的区块后15秒钟内发布时间戳,从而有效地使矿工预先计算出更有利于其彩票机会的期权。时间戳记不是随机的,因此不应在这种情况下使用.
15秒规则
这 黄纸 (以太坊的参考规范)未指定可在时间上漂移多少块的限制,但 它确实指定 每个时间戳都应大于其父时间戳。流行的以太坊协议实现 格思 和 平价 两者都拒绝时间戳超过15秒的块。因此,评估时间戳使用情况的一个好的经验法则是:如果与时间相关的事件的规模可以相差15秒并保持完整性,则使用block.timestamp是安全的.
避免将block.number用作时间戳
可以使用block.number属性和 平均封锁时间, 但这并不是未来的证据,因为封锁时间可能会改变(例如 叉重组 和 难度炸弹)。在几天的销售中,15秒规则使人们可以更可靠地估计时间.
看 SWC-116
多重继承注意
在Solidity中利用多重继承时,重要的是要了解编译器如何构成继承图.
合同最终{uint public a;函数Final(uint f)public {a = f; }}合同B是最终的{int公共费用;函数B(uint f)Final(f)public {}函数setFee()public {fee = 3; }}合同C为最终{int公共费用;函数C(uint f)Final(f)public {}函数setFee()public {fee = 5; }}合同A是B,C {函数A()public B(3)C(5){setFee();代码语言:PHP(php)
部署合同后,编译器将从右到左对继承进行线性化处理(在关键字是从最基本到最衍生的父级列出之后)。这是合约A的线性化:
最终的 <- 乙 <- C <- 一种
线性化的结果将产生5的手续费值,因为C是最衍生的合约。这看起来似乎很明显,但是可以想象一下C能够掩盖关键功能,对布尔子句进行重新排序并导致开发人员编写可利用合同的情况。当前,静态分析不会因功能过重而引起问题,因此必须手动对其进行检查.
为了帮助做出贡献,Solidity的Github提供了一个 项目 所有与继承有关的问题.
看 SWC-125
使用接口类型代替地址以确保类型安全
当函数将合同地址作为参数时,最好传递接口或合同类型而不是原始地址。如果在源代码中的其他位置调用该函数,则编译器将提供附加的类型安全保证.
在这里,我们看到两种选择:
合同验证者{函数validate(uint)外部收益(布尔); } contract TypeSafeAuction {//好的函数validateBet(Validator _validator,uint _value)内部收益(布尔){bool valid = _validator.validate(_value);返回有效; }} contract TypeUnsafeAuction {//错误的功能validateBet(地址_addr,uint _value)内部返回值(布尔){验证程序验证程序=验证程序(_addr);布尔有效= validator.validate(_value);返回有效; }}代码语言:JavaScript(javascript)
从下面的示例中可以看出使用上面的TypeSafeAuction合同的好处。如果使用地址参数或Validator以外的合同类型调用validateBet(),则编译器将抛出以下错误:
合同NonValidator {}合同拍卖是TypeSafeAuction {NonValidator nonValidator;函数下注(uint _value){bool有效= validateBet(nonValidator,_value); // TypeError:函数调用中的参数类型无效。 //从合同NonValidator //到请求的合同验证程序的无效隐式转换。 }}代码语言:JavaScript(javascript)
避免使用extcodesize检查外部拥有的帐户
通常使用以下修饰符(或类似的检查)来验证是从外部帐户(EOA)还是合同帐户进行呼叫:
//错误的修饰符isNotContract(address _a){uint size;程序集{size:= extcodesize(_a)} require(size == 0); _; }代码语言:JavaScript(javascript)
这个想法很简单:如果地址包含代码,则不是EOA,而是合同帐户。然而, 合同在施工过程中没有可用的源代码. 这意味着构造函数在运行时可以调用其他协定,但是其地址的extcodesize返回零。下面是一个最小的示例,显示了如何规避此检查:
合同OnlyForEOA {uint public flag; //错误的修饰符isNotContract(address _a){uint len;汇编{len:= extcodesize(_a)} require(len == 0); _; }函数setFlag(uint i)public isNotContract(msg.sender){flag = i; }}合同FakeEOA {构造函数(地址_a)public {OnlyForEOA c = OnlyForEOA(_a); c.setFlag(1); }}代码语言:JavaScript(javascript)
由于合同地址可以预先计算,因此如果检查在第n块为空但在大于n的某个块中部署了合同的地址,则此检查也可能失败.
警告: 这个问题是细微的。如果您的目标是阻止其他合同能够调用您的合同,则extcodesize检查可能就足够了。另一种方法是检查(tx.origin == msg.sender)的值,尽管这也可以 有缺点.
在某些情况下,extcodesize检查可以满足您的目的。在这里描述所有这些都超出了范围。了解EVM的基本行为并做出判断.
您的区块链代码安全吗?
与我们的安全专家预订为期1天的抽查。立即预订DiligenceSecuritySmart ContractsSolidityNewsletter订阅我们的时事通讯以获取最新的以太坊新闻,企业解决方案,开发人员资源等信息。网络研讨会
如何构建成功的区块链产品
网络研讨会
如何设置和运行以太坊节点
网络研讨会
如何构建自己的以太坊API
网络研讨会
如何创建社交令牌
网络研讨会
在智能合约开发中使用安全工具
网络研讨会