新闻开发人员企业区块链解释事件和会议新闻时事通讯
订阅我们的新闻.
电子邮件地址
我们尊重您的隐私
主页博客区块链开发
以太坊智能合约安全建议
从如何处理外部呼叫到承诺方案,在以太坊上构建时,将遵循以下10种智能合约安全模式。ConsenSys,2020年7月10日发布于2020年7月10日
正如我们在Smart Contract Security Mindset中所述,警惕的以太坊开发人员始终牢记五个原则:
- 为失败做准备
- 仔细推广
- 保持合同简单
- 保持最新
- 注意EVM的特性
在本文中,我们将深入探讨EVM的特质,并逐步介绍在以太坊上开发任何智能合约系统时应遵循的模式列表。这部分主要是针对中级以太坊开发人员的。如果您仍处于探索的早期阶段,请查看ConsenSys Academy的按需区块链开发人员计划.
好吧,让我们深入.
外部通话
拨打外部电话时请多加注意
调用不受信任的智能合约可能会带来一些意外的风险或错误。外部调用可能在该合同或它所依赖的任何其他合同中执行恶意代码。因此,将每个外部呼叫都视为潜在的安全风险。如果无法或不希望删除外部呼叫,请使用本节其余部分中的建议以最大程度地减少危险.
标记不信任的合同
与外部合同进行交互时,请以清楚表明与它们进行交互的潜在方式来命名变量,方法和合同接口。这适用于您自己的调用外部合同的功能.
//不良的Bank.withdraw(100); //不清楚受信任的函数还是不受信任的函数makeWithdrawal(uint amount){//尚不清楚此函数可能是不安全的Bank.withdraw(amount); } //好的UntrustedBank.withdraw(100); //不信任的外部调用TrustedBank.withdraw(100); //由XYZ Corp功能维护的外部但受信任的银行合同makeUntrustedWithdrawal(uint amount){UntrustedBank.withdraw(amount); }代码语言:PHP(php)
避免外部呼叫后状态改变
无论使用原始调用(形式为someAddress.call())还是合同调用(形式为ExternalContract.someMethod()),都假定可能执行恶意代码。即使ExternalContract不是恶意的,恶意代码也可以通过其调用的任何合同来执行.
一种特殊的危险是恶意代码可能会劫持控制流,从而由于可重入而导致漏洞。 (看 再入 对此问题进行更全面的讨论).
如果要呼叫不受信任的外部合同,请避免在呼叫后更改状态。这种模式有时也称为 检查效果互动模式.
看 SWC-107
不要使用transfer()或send().
.transfer()和.send()会将2,300瓦斯准确地转发给接收者。这种硬编码的气体津贴的目的是防止 再入漏洞, 但这仅在天然气成本不变的假设下才有意义. EIP 1884, 这是Istanbul硬叉的一部分,增加了SLOAD操作的汽油成本。这导致合同的后备功能要花费2300多种汽油。我们建议停止使用.transfer()和.send(),而改为使用.call().
//合同无效//无效{函数withdraw(uint256数量)外部{//这将转发2300瓦斯,如果接收方是合同且瓦斯成本发生变化,这可能不够。 msg.sender.transfer(amount); }} //良好的合同固定的{函数withdraw(uint256数量)外部{//这将转发所有可用的气体。请务必检查返回值! (bool成功,)= msg.sender.call.value(amount)("");要求(成功, "转移失败."); }}代码语言:JavaScript(javascript)
请注意,.call()对于减轻重入攻击没有任何作用,因此必须采取其他预防措施。为防止重入攻击,请使用 检查效果互动模式.
处理外部呼叫中的错误
Solidity提供了适用于原始地址的低级调用方法:address.call(),address.callcode(),address.delegatecall()和address.send()。这些低级方法从不抛出异常,但是如果调用遇到异常,则将返回false。另一方面,合同调用(例如,ExternalContract.doSomething())将自动传播一个引发(例如,如果doSomething()引发,则ExternalContract.doSomething()也将引发).
如果选择使用低级调用方法,请确保通过检查返回值来处理调用失败的可能性.
//糟糕的someAddress.send(55); someAddress.call.value(55)(""); //这是双重危险,因为它将转发所有剩余的气体,并且不检查结果someAddress.call.value(100)(bytes4(sha3("订金()"))); //如果存款抛出异常,则原始的call()将仅返回false,并且不会还原交易// //好(布尔成功,)= someAddress.call.value(55)(""); if(!success){//处理失败代码} ExternalContract(someAddress).deposit.value(100)();代码语言:JavaScript(javascript)
看 SWC-104
支持拨出用于外部呼叫的推送
外部呼叫可能会意外或故意失败。为了最大程度地减少此类故障造成的损害,通常最好将每个外部呼叫隔离到自己的事务中,该事务可以由呼叫的接收者发起。这与付款特别相关,在这种情况下,最好让用户提取资金,而不是自动将资金推给他们。 (这也减少了 气体限制问题.)避免在一次交易中合并多个以太坊转移.
//错误的合同拍卖{地址highestBidder;最高最高出价;函数bid()应付帐款{require(msg.value >= maximumBid); if(highestBidder!= address(0)){(布尔成功,)= maximumBidder.call.value(highestBid)("");要求(成功); //如果此调用始终失败,则没有其他人可以出价} maximumBidder = msg.sender; maximumBid = msg.value; }} //好的合同拍卖{地址highestBidder;最高最高出价;映射(地址=> uint)退款;函数bid()外部应付款{require(msg.value >= maximumBid);如果(最高出价!=地址(0)){退款[最高出价] + =最高出价; // //记录该用户可以要求退款的金额} maximumBidder = msg.sender; maximumBid = msg.value; }函数withdrawRefund()外部{uint退款=退款[msg.sender];退款[msg.sender] = 0; (成功成功)= msg.sender.call.value(refund)("");要求(成功); }}代码语言:JavaScript(javascript)
看 SWC-128
不要将调用委托给不受信任的代码
该委托调用函数从其他合同中调用函数,就好像它们属于调用者合同一样。因此,被叫方可以更改主叫地址的状态。这可能是不安全的。下面的示例显示了使用委托调用如何导致合同被破坏并失去余额的情况.
合同析构函数{函数doWork()外部{selfdestruct(0); }} Contract Worker {function doWork(address _internalWorker)public {//不安全的_internalWorker.delegatecall(bytes4(keccak256("做工作()"))); }}代码语言:JavaScript(javascript)
如果使用已部署的Destructor合同的地址作为参数调用Worker.doWork(),则Worker合同将自毁。仅将执行委托给受信任的合同,并且 永远不要到用户提供的地址.
警告
不要假设合同的余额为零。攻击者可以在创建合同之前将以太币发送到合同的地址。合同不应假定其初始状态包含零余额。看 第61期 更多细节.
看 SWC-112
请记住,可以强制将以太币发送到一个帐户
谨防对严格检查合同余额的不变式进行编码.
攻击者可以强制将以太币发送到任何帐户。这是无法避免的(即使使用执行revert()的后备函数也无法避免).
攻击者可以通过创建合同,使用1 wei为其提供资金并调用selfdestruct(victimAddress)来实现此目的。没有在受害者地址中调用任何代码,因此无法避免。发送到矿工的地址的区块奖励也是如此,该地址可以是任意地址.
另外,由于可以预先计算合同地址,因此可以在部署合同之前将以太币发送到某个地址.
看 SWC-132
请记住,链上数据是公开的
许多应用程序要求提交的数据必须保密,直到某个时间点才能正常工作。游戏(例如,链上的石头剪刀布)和拍卖机制(例如,密封竞标) Vickrey拍卖会)是示例的两个主要类别。如果您要在隐私问题上构建应用程序,请确保避免要求用户过早发布信息。最好的策略是使用 承诺方案 分阶段进行:首先使用值的哈希值提交,然后在后续阶段中显示值.
例子:
- 在剪刀石头布上,要求两个玩家先提交其预期动作的哈希值,然后要求两个玩家均提交其动作;如果提交的动作与哈希值不匹配,则将其丢弃.
- 在拍卖中,要求玩家在初始阶段提交其出价值的哈希值(以及大于其出价值的保证金),然后在第二阶段提交其出价值.
- 开发依赖于随机数生成器的应用程序时,顺序应始终为(1)玩家提交动作,(2)生成随机数,(3)玩家支付。许多人正在积极研究随机数发生器。当前同类最佳的解决方案包括比特币区块头(已通过 http://btcrelay.org),散列提交显示方案(即,一方生成一个数字,发布其散列以“提交”给该值,然后在以后显示该值)和 兰道. 由于以太坊是确定性协议,因此您不能在协议中使用任何变量作为不可预测的随机数。还请注意,矿工在某种程度上控制着block.blockhash()的值*.
注意某些参与者可能“下线”而不返回的可能性
不要依赖于由特定方执行特定操作的退款或索赔程序,而没有其他方法可以取出资金。例如,在石头剪刀布游戏中,一个常见的错误是在两个玩家都提交动作之前不进行支付。但是,恶意的玩者可以通过根本不提交自己的举动来“骚扰”对方-实际上,如果一个玩者看到了对方显示的举动并确定自己输了,则根本没有理由提出自己的举动。在国家渠道解决的情况下,也可能会出现此问题。当此类情况成为问题时,(1)提供一种规避未参与参与者的方法,可能会在一定时限内进行;(2)考虑为参与者在其所处的所有情况下提交信息提供额外的经济激励应该这样做.
提防负数最大的负整数
实体提供了几种与带符号整数一起使用的类型。与大多数编程语言一样,在Solidity中,具有N位的带符号整数可以表示-2 ^(N-1)到2 ^(N-1)-1的值。这意味着MIN_INT没有正等价物。负数的实现是找到一个数字的两个补数,因此,最负数的否定 将得出相同的数字. 对于Solidity中的所有有符号整数类型都是如此(int8,int16,…,int256).
合同否定{函数negate8(int8 _i)公共纯收益(int8){return -_i; }函数negate16(int16 _i)公共纯返回值(int16){return -_i; } int8 public a = negate8(-128); // -128 int16 public b = negate16(-128); // 128 int16 public c = negate16(-32768); // -32768}代码语言:PHP(php)
处理此问题的一种方法是,在否定之前检查变量的值,并在变量值等于MIN_INT时抛出该值。另一种选择是确保通过使用容量更大的类型(例如int32而不是int16)永远不会获得最大的负数。.
当MIN_INT乘以或除以-1时,会发生与int类型类似的问题.
您的区块链代码安全吗?
我们希望这些建议对您有所帮助。如果您和您的团队正在准备启动甚至在开发生命周期的开始,并且需要对智能合约进行完整性检查,请随时与ConsenSys Diligence的安全工程师团队联系。我们在这里帮助您以100%的信心启动和维护您的以太坊应用程序.
预订安全抽查
与我们的区块链安全专家团队进行为期1天的审查。立即预订您的SecuritySmart合约新闻快讯订阅我们的新闻快讯,以获取最新的以太坊新闻,企业解决方案,开发人员资源等信息。网络研讨会
如何构建成功的区块链产品
网络研讨会
如何设置和运行以太坊节点
网络研讨会
如何构建自己的以太坊API
网络研讨会
如何创建社交令牌
网络研讨会
在智能合约开发中使用安全工具
网络研讨会