首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安全的处理 ERC20 转账(解决非标准 ERC20 问题)

安全的处理 ERC20 转账(解决非标准 ERC20 问题)

作者头像
Tiny熊
发布2021-10-13 11:32:16
1.7K0
发布2021-10-13 11:32:16
举报

  • 译文出自:登链翻译计划[1]
  • 译者:翻译小组[2]
  • 校对:Tiny 熊[3]

你可能认为在 ERC-20 调用几个函数非常简单,对吗?很不幸,不是的。有些事情我们必须要考虑,而且还可能出现一些很常见的问题。

我们从最简单的开始,下面我们要处理一个非常普通的 token 交易,下面的代码会导入并直接使用 IERC20.sol。

怎样安全的处理 ERC20 转账

// 不正确的版本
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";

function interactWithToken(uint256 sendAmount) {
  // some code
  IERC20 token = IERC20(tokenAddress);
  token.transferFrom(msg.sender, address(this), sendAmount);
}

对于像DAI[4]这样的 token 来说这段代码是很完美的,调用 transfer 函数并在出错的时候回退调用。

但是,如果我们调用的是 0x(ZRX)会发生什么?ZRX代码在这里[5]

function transferFrom(address _from, address _to, uint _value) returns (bool) {
        if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
            balances[_to] += _value;
            balances[_from] -= _value;
            allowed[_from][msg.sender] -= _value;
            Transfer(_from, _to, _value);
            return true;
        } else { return false; }
}

我们可以看到,与DAI不同,当出错时 0x 不会回退交易,而是返回 false,但是我们在代码中不管这个返回值。本质上,任何人都可以与我们合约的interactWithToken交易,合约会认为成功交易了一个 token ,但实际上什么也没有做。很糟糕!

ZRX 仍然符合 ERC-20 标准,因为没有任何地方规定 ERC-20 合约必须在发生失败时回退交易。这两种方法都有优点和缺点。在上面的例子中,很明显我们只需要检查返回值就知道是否成功,一段简单的代码 require(token.transferFrom(msg.sender, address(this), sendAmount), "Token transfer failed!"); 就可以修复。合约所有函数都是这样,执行失败的时候返回 false 或者回退,所以,一定要处理好这两种情况。

合约内部的错误处理

大多数情况下,token 会在失败时回退交易。这样做的好处是,即使是像我们的第一个例子那样的代码,仍然可以安全地交易。这就是为什么 OpenZeppelin 的 ERC20 (代码[6])实现中这样做,也是我建议这样做的原因。

而对于返回值的做法,是有争议的。如果你知道正在交易的 token 在失败时返回 false,或许你只会想为这些 token 添加额外的功能,则可以像下面的例子一样处理:

function interactWithToken(uint256 sendAmount) {
  IERC20 token = IERC20(tokenAddress);
  bool success = token.transferFrom(msg.sender, address(this), sendAmount);

  if (success) {
    // handle success case
  } else {
     // handle failure case without reverting
  }
}

这样的好处显然是,即使 token 转移失败,我们仍然允许交易成功。

如果 token 在失败时回退交易,错误如何处理?

这在以前是比较复杂的,但从 Solidity 0.6 之后,就已经不那么困难了,现在 Solidity 支持try/catch[7]

function interactWithToken(uint256 sendAmount) {
  IERC20 token = IERC20(tokenAddress);
  bool success;
  
  try token.transferFrom(msg.sender, address(this), sendAmount) returns (bool _success) {
    success = _success;
  } catch Error(string memory /*reason*/) {
    success = false;
    // special handling depending on error message possible
  } catch (bytes memory /*lowLevelData*/) {
    success = false;
  }

  if (success) {
    // handle success case
  } else {
     // handle failure case without reverting
  }
}

这样你就可以为两个版本的 ERC-20 合约做错误处理。

怎样支持所有 token

现在你已经支持了 ERC-20 标准的 token, 然而有相当多的 token 看起来像 ERC-20 ,但是它的有些行为却不像,有些出现缺少返回值的错误[8]

有一段时间,OpenZeppelin 有一个bug,他们在失败的时候回退交易,但没有在成功时返回 true(即缺少返回值)。这个 bug 让很多 token 都受到了影响,包括 USDT、OmiseGo 和 BNB 。你期望返回一个布尔值,却没有任何值返回,这种情况,如果用 Solidity 0.4.22 或更高版本编译,会回退交易,这个 bug 甚至影响到了Uniswap[9]

那么其他项目是如何处理这个问题的呢?我们看看下面的Compound 版本[10]

function doTransferOut(address payable to, uint amount) internal {
    EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying);
    token.transfer(to, amount);

    bool success;
    assembly {
        switch returndatasize()
            case 0 {                      // This is a non-standard ERC-20
                success := not(0)          // set success to true
            }
            case 32 {                     // This is a complaint ERC-20
                returndatacopy(0, 0, 32)
                success := mload(0)        // Set `success = returndata` of external call
            }
            default {                     // This is an excessively non-compliant ERC-20, revert.
                revert(0, 0)
            }
    }
    require(success, "TOKEN_TRANSFER_OUT_FAILED");
}

其先检查返回数据的大小,如果是 0 ,我们就假定它是行为不正常的 token 。如果调用没有回退交易,那就意味着交易成功了,应该返回 true 。

随着 Solidity 的版本更新,我们可以简化这段代码,像Uniswap是这样做的[11]

function safeTransfer(address token, address to, uint value) internal {
  // bytes4(keccak256(bytes('transfer(address,uint256)')));
  (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
  require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');
}

这种实现方法只是稍有不同而已,因为 abi.decode 也会对其他 data.lengths 起作用,不是只有32 字节,但是这没关系,可以很容易修改以支持错误处理:

function safeTransferNoRevert(address token, address to, uint value) internal returns (bool) {
  (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
  return success && (data.length == 0 || abi.decode(data, (bool));
}

你应该怎么做?

那么,现在最好的方法是什么呢?一个很简单的方法就是,使用OpenZeppelin SafeERC20[12]来实现。

这是一个围绕 ERC-20 调用的包装库。不要感到困惑,这不是为了创建自己的 token ,而是为了安全地交易。SafeERC20 的实现基本上就是像上面的 Uniswap 版本一样,你可以像下面这样用它:

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/SafeERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";

contract TestContract {
    using SafeERC20 for IERC20;

    function safeInteractWithToken(uint256 sendAmount) external {
        IERC20 token = IERC20(address(this));
        token.safeTransferFrom(msg.sender, address(this), sendAmount);
    }
}

本翻译由 Cell Network[13] 赞助支持。

来源:https://soliditydeveloper.com/safe-erc20

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

DAI: https://etherscan.io/address/0x6b175474e89094c44da98b954eedeac495271d0f#code

[5]

这里: https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code

[6]

代码: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol

[7]

try/catch: https://solidity.readthedocs.io/en/latest/control-structures.html#try-catch

[8]

缺少返回值的错误: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca

[9]

影响到了Uniswap: https://twitter.com/UniswapProtocol/status/1072286773554876416

[10]

Compound 版本: https://github.com/compound-finance/compound-money-market/blob/241541a62d0611118fb4e7eb324ac0f84bb58c48/contracts/SafeToken.sol#L97

[11]

Uniswap是这样做的: https://github.com/Uniswap/uniswap-lib/blob/9642a0705fdaf36b477354a4167a8cd765250860/contracts/libraries/TransferHelper.sol#L13-L17

[12]

OpenZeppelin SafeERC20: https://docs.openzeppelin.com/contracts/3.x/api/token/erc20#SafeERC20

[13]

Cell Network: https://www.cellnetwork.io/?utm_souce=learnblockchain

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 怎样安全的处理 ERC20 转账
  • 合约内部的错误处理
  • 怎样支持所有 token
  • 你应该怎么做?
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档