安全审计中常见的非标准ERC20(即Weird ERC20)问题

  • SmileBits
  • 更新于 2024-04-27 09:40
  • 阅读 1013

ERC20 标准的核心是指定了代币合约应实现的一组函数和事件,但它并没有规定这些函数应如何处理故障。 例如, transfer 和 transferFrom 函数用于在帐户之间移动代币,传统上返回指示成功或失败的布尔值。然而,并非所有实现都严格遵守此模式。 有些人可能会选择在失败时恢复(即抛出错误并

了解 ERC20 代币以及严格标准的必要性

ERC20 标准通过以太坊改进提案 20 (EIP-20) 引入,概述了转移代币和授权许可等关键功能。 尽管该标准使得数字资产创造激增,但它也有其局限性。 首先,ERC20 仅提出指导方针,而不是可执行的规则。 这导致开发人员对这些指南的实施方式多种多样,进而导致了不一致。 这些不一致的一个关键方面是错误处理机制,某些Token可能会在交易失败时返回错误值,而其他Token可能会完全恢复交易。 缺乏统一的方法可能会导致不安全的操作,因为应用程序可能无法统一地预测或处理故障,从而可能导致资金损失或其他安全问题。

关键点——没有与 ERC20 代币安全交互的标准方法

ERC20 标准为代币创建提供了蓝图,彻底改变了生态系统。然而,它在安全性和可靠性方面还有很多不足之处。 最紧迫的问题之一是缺乏与这些代币安全交互的统一方法。 标准中的这种差距导致了多种实施方式。

问题的核心

ERC20 标准的核心是指定了代币合约应实现的一组函数和事件,但它并没有规定这些函数应如何处理故障。 例如, transfer 和 transferFrom 函数用于在帐户之间移动代币,传统上返回指示成功或失败的布尔值。然而,并非所有实现都严格遵守此模式。 有些人可能会选择在失败时恢复(即抛出错误并撤消所有更改)。这种不一致可能会导致开发人员对代币的行为做出错误的假设,从而导致与这些代币交互的智能合约中出现错误和漏洞。

一个真实案例

这就是原始 EIP 的功能接口Original EIP

    function transfer(address _to, uint256 _value) public returns (bool success)

为了说明潜在的危险,让我们检查一下JuiceBox 在 Code4rena 审计中发现的问题(保存在 Solodit 中)。 这是一个内部“_transferFrom”函数:

   function _transferFrom(
    address _from,
    address payable _to,
    uint256 _amount
  ) internal override {
    _from == address(this)
      ? IERC20(token).transfer(_to, _amount)
      : IERC20(token).transferFrom(_from, _to, _amount);
  }

在此代码中,开发人员使用通用的 IERC20 接口来处理常规 ERC20。 然而,由于该接口遵循ERC20标准,因此它需要一个布尔返回值。 该函数不适用于许多流行的 ERC20(USDT、BNB..),因为它们不返回任何值。——调用IERC20(token).transfer或者IERC20(token).transferFrom 会报错。 这就是 BNB“转移函数”的样子:

function transfer(address _to, uint256 _value) { // Doesn't return any value
        if (_to == 0x0) throw;
        if (_value <= 0) throw;
        if (balanceOf[msg.sender] < _value) throw;
        if (balanceOf[_to] + _value < balanceOf[_to]) throw;
        balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value);
        balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value);                     
        Transfer(msg.sender, _to, _value);
    }

可以看到,无论转账成功与否,都没有返回值。如果不满足转移条件,则只是“throw”。

使用SafeERC20 以实现安全代币交互

这就是 OpenZeppelin 的所在地 安全ERC20 库发挥作用,为安全地与 ERC20 代币交互提供了一个强大的框架。 它允许开发人员更安全地管理代币传输。 与静默失败或错误时不恢复的标准方法不同,低级调用可以显式处理返回值 这意味着,如果代币合约未按预期执行,您的合约可以有效地检测并处理这种情况。

 function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

 function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(data);
        if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

所以 SafeERC20::safeTransfer 将调用代币的转账函数。 如果该函数没有返回(来自Weird ERC20),它也会执行。 知道如果出现错误它会revert,即统一处理了是否有返回值的情况。 因此,现在如果传输失败,库将抛出错误,因此开发人员不必再手动检查返回值。

使用SafeERC20

集成 SafeERC20 很简单。以下是如何将其合并到您的开发过程中的方法:

Copy

    import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

    // And inside your contract
    using SafeERC20 for IERC20;

通过采用 SafeERC20 ,您可以利用 OpenZeppelin 的广泛测试和社区反馈,这可以显着增强区块链应用程序的安全性。 请记住,在不断发展的区块链技术领域,安全的重要性怎么强调都不为过。

使用OpenZeppelin的库时请理解其底层实现 不然也会引入Bug,以SafeERC20::safeApprove为例,(4.x版本中有这个问题,5版本里面就没有了)

   function safeApprove(IERC20 token, address spender, uint256 value) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

safeApprove内要求allowance(address(this), spender)为0,不为0时就会revert.你看它注释里其实也提示了。所以在函数里面调用safeApprove,是容易出现DoS(Deny of Service)

参考资料:

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

0 条评论

请先 登录 后评论
SmileBits
SmileBits
智能合约安全审计