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