睿象云智能告警平台的分派策略
803
2023-01-07
如何保护智能合约不被重入攻击
即将于12月初推出的伊斯坦布尔硬分叉包括EIP1884:“限制trie大小有关的操作码”。关键字是“限制”,这意味着某些指令现在将花费更多的气体来执行。最近对此进行了很多讨论的原因是现有的可以少量气体运行代码,硬分叉后可能超过该限制,并导致会出现“out of gas”错误。
“以少量气体运行代码”的一个特例是任何Solidity智能合约中的fallback函数,因为它是由Solidity的transfer函数或Solidity和Vyper的send函数触发的以太坊传递过程中运行的代码。transfer和send都只允许以太坊的接收者以2300的气体(实际上是零)。伊斯坦布尔到分叉后,接近此限制的fallback函数可能会停止工作,而任何调用这些函数的智能合约都将在有限的气体中停止工作。
本文介绍了可重入性,目前可用于根据它获得智能合约的技术,以及如何使用OpenZeppelin合约轻松在项目中实现它们。特别值得一提的是我们还没有提到的一种技术:提款支付法(pull payments)。
什么是可重入攻击?
智能合约在正常执行期间可以通过执行函数调用或简单地转移以太坊来执行对其他智能合约的调用。这些智能合约本身可以称为其他智能合约。特别是它们可以回调到调用他们的智能合约或回调栈中的任何其他智能合约。在这种情况下,我们说智能合约被重新输入,这种情况被称为可重入性。
重入本身不是问题。当智能合约以“不一致”状态重新输入时,就会出现问题。当智能合约特定的不变量成立时,状态被认为是一致的。例如对于ERC20主要不变性是所有智能合约余额的总和不超过已知的总供应量。
通常函数假定它们开始运行时便以一致的状态观察智能合约,并且它们还承诺一旦完成运行就使智能合约保持一致。在执行过程中,可能会违反不变量,这很好,只要没有人能观察到不一致的状态。问题在于通过重入,这成为可能。函数完成时,不仅要保持不变量,还必须在每个潜在的重入点保持不变。
当我们调用不受信任的智能合约或将资金转入不受信任的帐户时,我们的代码容易受到重入攻击的攻击。可以对这些帐户进行特殊编程,以在重入调用期间滥用不变违规。
这里的不变之处在于,智能合约中的资金额等于余额映射中所有条目的总和。在第三行执行调用期间,由于_amount资金已转出,但余额尚未更新,因此不变量被破坏了。 由于msg.sender可以是智能合约,因此同一调用允许重入。 如果攻击者此时触发了重入,他们将能够从破碎的不变量中获利。
msg.sender.call.value(_amount)();
balances[msg.sender] -= _amount;
}
}
现在,我们将看到几种抵御这些攻击的方法。
已经对这种模式进行了很多讨论,您应该在Solidity文档中和ConsenSys的最佳实践中对其进行阅读。
但是我们应该对这种方法不满意,因为它容易受到人为错误的影响:程序员必须正确地应用它,而审阅者必须发现任何错误。是否可以减轻穷人的这种责任?
ReentrancyGuard(重入保护)
如果在执行的任何时候不确定智能合约的不变量是否成立,则应避免调用其他(不可信)智能合约,因为它们可能会被重入。 如果我们别无选择,可以尝试使用ReentrancyGuard来防止可重入。
ReentrancyGuard(重入保护)是一段代码,当检测到重入时,该执行会导致执行失败。OpenZeppelin合约中有一个称为ReentrancyGuard(重入保护)的模式实现,该模式提供了nonReentrant修饰符。将此修饰符应用于函数将使其变为“不可重入”,并且通过重新调用将拒绝重新输入该函数的尝试。
当我们的智能合约具有多个函数时会发生什么? 由于修饰符是针对每个函数的应用,因此如果要完全防止重入攻击,则必须将其应用于所有函数。否则如果它对不可变的变量很敏感,仍然有可能重新进入另一个函数并将其用于重入攻击。
尽管有所有注意事项,但在某些情况下,重入防护(reentrancy guards)可能会很有价值。但是要完全消除可重入性也有其弊端:在某些情况下,可重入性是安全的,并且随着以太坊智能合约变得更加复杂,可组合和相互联系,我们可能会在外看到它的合法用途。
Pull Payments(提款支付法)
如果我们将Ether转移到合约中但未执行其代码,则根本无法重入。通过使用selfdestruct,可以在EVM中绕过接收器的代码。但是接收以太币的合约需要以某种方式进行处理,并且大多数没有编程为处理通过自毁而收到的资金,这可能导致资金损失。
另一种选择是提款支付模式(pull payment )。这个想法是与其将资金“推”到接收者,不如将它们“拉”出合约。 OpenZeppelin合约在PullPayment合约中实现了这种模式。继承此协定将提供类似于传递的内部函数_asyncTransfer。但是它不会将资金发送给接收方,而是将其转移到托管合约中。此外PullPayment还为接收者提供Public函数以提取其付款:withdrawPayments和withdrawPaymentsWithGas。
OpenZeppelin Contracts 2.4中添加了第二个命令withdrawPaymentsWithGas,以修复伊斯坦布尔的操作码重新定价,并在实际的以太坊转移过程中将所有可用的气体转发给接收器。请注意此时可以重新输入,但这是安全的,因为PullPayment(提款支付法)不会使您的合约的任何不变式无效。
值得一提的是,提款功能可以由任何人调用,而不仅仅是接收方。这意味着收款人无需知道这是预付款的目标,这在现有的智能合约无法自行付款时尤其重要。
总 结
发表评论
暂时没有评论,来抢沙发吧~