ERC20 标准的核心是指定了代币合约应实现的一组函数和事件,但它并没有规定这些函数应如何处理故障。 例如, transfer 和 transferFrom 函数用于在帐户之间移动代币,传统上返回指示成功或失败的布尔值。然而,并非所有实现都严格遵守此模式。 有些人可能会选择在失败时恢复(即抛出错误并
ERC20 标准通过以太坊改进提案 20 (EIP-20) 引入,概述了转移代币和授权许可等关键功能。 尽管该标准使得数字资产创造激增,但它也有其局限性。 首先,ERC20 仅提出指导方针,而不是可执行的规则。 这导致开发人员对这些指南的实施方式多种多样,进而导致了不一致。 这些不一致的一个关键方面是错误处理机制,某些Token可能会在交易失败时返回错误值,而其他Token可能会完全恢复交易。 缺乏统一的方法可能会导致不安全的操作,因为应用程序可能无法统一地预测或处理故障,从而可能导致资金损失或其他安全问题。
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”。
这就是 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 很简单。以下是如何将其合并到您的开发过程中的方法:
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)
参考资料:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!