• 译文出自:登链翻译计划 [1]

  • 译者:翻译小组 [2]

  • 校对:Tiny 熊 [3]

智能合约可以部署其他智能合约,通常称为 工厂模式 ),让你不是创建一个合约跟踪很多事情,而是创建多个智能合约,每个合约只跟踪各个的事情。使用这种模式可以简化合约代码,减少某些类型的安全漏洞的影响。

在这篇文章中,我将带你了解一个例子,这个例子是基于最近的一次审计中发现的一个关键漏洞修改而来。如果使用了工厂模式,这个漏洞就不会那么严重了。

一个错误的智能合约

下面是一个智能合约,通过一个相当简单的接口来出售 WETH。如果你有 WETH,你只需要 approve (授权) 这个智能合约来出售你的代币,它将确保你得到正确的金额。只要批准了足够的代币,任何人都可以向你购买 WETH 。

合约采用提现模式 [4] 向卖家交付出售所得的 ETH,但合约作者却犯了严重错误,代码如下:

     // 技术上可以实现出售任何代币,但这个例子仅出售  WETH 。        // 因为这里不想关注价格 . 1 WETH = 1 ETH.       contract WETHMarket {           IERC20 public weth;           mapping(address => uint256) public balanceOf;           constructor(IERC20_weth) public {               weth =_weth;           }          // 从指定的 seller 购买 WETH . seller 需要先授权 WETH.          function buyFrom(address seller) external payable {              balanceOf[seller] += msg.value;              require(weth.transferFrom(seller, msg.sender, msg.value),                  "WETH transfer failed.");          }          // 出售者调用,提取 ETH          function withdraw(uint256 amount) external {              require(amount <= balanceOf[msg.sender], "Insufficient funds.");              // Whoops! Forgot this:              // balanceOf[msg.sender] -= amount;              (bool success, ) = msg.sender.call.value(amount)("");              require(success, "ETH transfer failed.");          }      }  

如果你想知道为什么代码使用 .call 而不是 .transfer ,请阅读停止使用 transfer()[5])。

由于卖方的余额永远不会减少,所以,一个有以太币余额的卖方,只需反复调用 withdraw() ,就可以将合约中所有人的以太币耗尽。这是一个严重的漏洞。

修复这个 bug,就像大多数 bug 一样,一旦你发现了它,就会变得很容易。但在这篇文章中,我想谈谈如何通过使用工厂模式来减轻这个 bug,即使我们对这个问题一无所知。

更小责任的合约

现在让我们来看一个更简单的 WETHMarket 合约的版本。在这个版本中,合约只负责销售单个卖家的 WETH。这个合约与之前的版本有同样的 BUG。

     contract WETHSale {           IERC20 public weth;           address seller; // 仅对一个 seller 有效           uint256 public balance; // 不在需要 mapping           constructor(IERC20_weth, address_seller) public {               weth =_weth;               seller =_seller;           }          // 不用再指定 seller.          function buy() external payable {              balance += msg.value;              require(weth.transferFrom(seller, msg.sender, msg.value));          }          function withdraw(uint256 amount) external {              require(msg.sender == seller, "Only the seller can withdraw.");              require(amount <= balance, "Insufficient funds.");              uint256 amount = balance;              // Whoops! Forgot this:              // balance -= amount;              (bool success, ) = msg.sender.call.value(amount)("");              require(success, "ETH transfer failed.");          }      }  

使用工厂提高智能合约安全性