Solidity 智能合约安全性:防止重入攻击的 4 种方法

有三种主要的技术可以防止重入:检查、效果、交互(CEI),重入保护/互斥,提款支付。此外,最后一种方法可能是有效的,但不推荐:限制gas

1.png

可重入是一种编程技术,在这种技术中,函数的执行会被外部函数调用中断。在外部函数调用的逻辑中,有一些条件允许它在原始函数执行完成之前递归地调用自身。在某些情况下,可能需要反复重新进入流程以执行外部逻辑,但不一定是错误。但是,不建议将此技术用于智能合约,因为它将控制流执行释放给一个可能试图利用资金的不受信任的合约。此外,在执行对外部合约的调用时,应该使用反重入模式和防护措施来防止这种类型的攻击发生。

有三种主要的技术可以防止重入:

  • 检查、效果、交互(CEI)
  • 重入保护/互斥
  • 提款支付

此外,最后一种方法可能是有效的,但不推荐:

  • 限制gas

检查、效果、交互

CEI模式是防止重入的一种简单而有效的方法。检查是指条件的真实性。效果是指交互导致的状态修改。最后,交互指的是函数或合约之间的交易。

下面是一个不应该尝试的例子(效果之前的交互):

// contract_A: holds user's fundsfunction withdraw() external {
  uint userBalance = userBalances[msg.sender];  require(userBalance > 0);  (bool success,) = msg.sender.call{ value: userBalance }("");
  require(success,);  userBalances[msg.sender] = 0;
}

下面是攻击者的接收函数:

// contract_B: reentrancy attackreceive() external payable {
  if (address(contract_A).balance >= msg.value) {
    contract_A.withdraw();
  }

}

攻击者的接收函数接收到取款资金,应该只返回“success”,而不是检查contract_A是否包含更多资金。如果为真,contract_B将再次递归调用withdraw函数,直到所有资金用完为止。

下面是一个使用CEI模式的提取函数的示例:

function withdraw() external {
  uint userBalance = userBalances[msg.sender];  require(userBalance > 0);  userBalances[msg.sender] = 0;  (bool success,) = msg.sender.call{ value: userBalance }("");
  require(success,);
}

在将资金转移到contract_B之前,会先将用户在contract_A中的账户余额归零,在contract_B发起重入攻击时,withdraw函数中的条件将为假,执行将被恢复。

重入保护/互斥

重入保护或互斥(互斥标志)可以构造为函数或函数修饰符,但逻辑很简单:在易受重入攻击的函数调用周围放置一个布尔锁。“锁定”的初始状态是false(未锁定),但是在易受攻击的函数开始执行之前,它被设置为true(已锁定),然后在函数结束后又被设置为false(未锁定)。

下面是一个使用上述withdraw函数的示例:

bool internal locked = false;function withdraw() external {
  require(!locked);
  locked = true;  uint userBalance = userBalances[msg.sender];
  require(userBalance > 0);
  (bool success,) = msg.sender.call{ value: userBalance }("");
  require(success,);
  userBalances[msg.sender] = 0;  locked = false;

}

这个withdraw函数不遵循CEI模式,因此很容易受到重入攻击,但是简单的布尔“锁定”变量可以防止重入,因为第一个require语句将等同于false并恢复交易。

提款支付

最后一种技巧是Open Zeppelin推荐的最佳实践。然而,在自动化方面有一个轻微的权衡。提款支付通过中介托管和避免与潜在的敌对合约直接接触来实现安全。

在这里,合约资金被发送给中介托管:

function sendPayment(address user, address escrow) external {
  require(msg.sender == authorized);  uint userBalance = userBalances[user];  require(userBalance > 0);  userBalances[user] = 0;  (bool success,) = escrow.call{ value: userBalance }("");
  require(success,);

}

在这里,托管资金可以由接收者提取:

function pullPayment() external {
  require(msg.sender == receiver);  uint payment = account(this).balance;  (bool success,) = msg.sender.call{ value: payment }("");
  require(success,);

}

通过中介托管发送资金,合约资金可以免受重入攻击。如果托管为多个账户持有资金,则托管可能会受到重入,因此应在合适的情况下实施 CEI 模式和/或重入保护。

gas限制

最后,gas限制可以防止重入攻击,但这不应被视为一种安全策略,因为gas成本取决于以太坊的操作码,而这些操作码可能会发生变化。另一方面,智能合约代码是不可变的。有必要了解函数之间的区别:sendtransfercall

函数 send 和 transfer 本质上是相同的,但是如果交易失败,transfer将恢复,而send则不会。

// transfer will revert if the transaction 
failsaddress(receiver).transfer(amount);// send will not revert if the transaction failsaddress(receiver).send(amount);

关于重入,send 和 transfer都有2300个单位的gas限制。使用这些函数应该可以防止发生重入攻击,因为没有足够的gas递归来调用原始函数来利用资金。

与send 和 transfer不同,调用没有gas限制,而且会将其gas转发,以执行复杂的多合约交易。当然,后者也包括重入攻击。

结论

成功的重入攻击可能是毁灭性的,并可能耗尽受害者合约中的所有资金,因此了解潜在的漏洞并实施有效的保障措施是很重要的。

CEI模式应该是默认实现,无论是否存在漏洞。额外的安全性可以通过使用重入保护和/或提款支付来实现。最后,gas限制可能会防止重入,但不应被视为一种安全策略。

Source:https://medium.com/better-programming/solidity-smart-contract-security-preventing-reentrancy-attacks-fc729339a3ff

关于

ChinaDeFi - ChinaDeFi.com 是一个研究驱动的DeFi创新组织,同时我们也是区块链开发团队。每天从全球超过500个优质信息源的近900篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。

本文首发于:https://mp.weixin.qq.com/s/9SR5Y10WbDxGfzjQ0_Rbhg

点赞 0
收藏 6
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ChinaDeFi 去中心化金融社区
ChinaDeFi 去中心化金融社区
ChinaDeFi.com 是一个研究驱动的DeFi创新组织,同时我们也是区块链开发团队。每天从全球超过500个优质信息源的近900篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。