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

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

作者头像
Tiny熊
发布于 2021-10-13 03:32:16
发布于 2021-10-13 03:32:16
2K00
代码可运行
举报
运行总次数:0
代码可运行

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

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

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

怎样安全的处理 ERC20 转账

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 不正确的版本
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]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 添加额外的功能,则可以像下面的例子一样处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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]

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 字节,但是这没关系,可以很容易修改以支持错误处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 版本一样,你可以像下面这样用它:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
MySql中的锁
锁是协调多个进程或者线程访问某一共享资源的机制。数据库是用来保存数据的,自然其中也有锁机制。对整个数据库加锁,对某一整张表加锁,对某一记录行加锁,对应了锁粒度的从粗到细。
Eulogy
2025/07/27
190
MySQL锁、加锁机制(超详细)—— 锁分类、全局锁、共享锁、排他锁;表锁、元数据锁、意向锁;行锁、间隙锁、临键锁;乐观锁、悲观锁
客户端发往MySQL的一条条SQL语句,实际上都可以理解成一个个单独的事务(一条sql语句默认就是一个事务)。而事务是基于数据库连接的,每个数据库连接在MySQL中,又会用一条工作线程来维护,也意味着一个事务的执行,本质上就是一条工作线程在执行,当出现多个事务同时执行时,这种情况则被称之为并发事务,所谓的并发事务也就是指多条线程并发执行。
寻求出路的程序媛
2024/06/24
23.9K5
MySQL锁、加锁机制(超详细)—— 锁分类、全局锁、共享锁、排他锁;表锁、元数据锁、意向锁;行锁、间隙锁、临键锁;乐观锁、悲观锁
【MySQL】一文带你搞懂MySQL中的各种锁
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资( CPU 、
陶然同学
2023/10/14
2.2K0
【MySQL】一文带你搞懂MySQL中的各种锁
MySQL数据库原理学习(三十八)
MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免DML与DDL冲突,保证读写的正确性。
用户1289394
2023/01/05
3210
MySQL数据库原理学习(三十八)
【MySQL-25】万字总结<锁>——(全局锁&行级锁&表级锁)【共享锁,排他锁】【间隙锁,临键锁】【表锁,元数据锁,意向锁】
默认情况下,InnODB在 REPEATABLE READ事务隔离级别运行,InnoDB使用 临键锁 进行搜索和索引扫描,以防止幻读。(本次演示)
YY的秘密代码小屋
2024/09/09
3010
【MySQL-25】万字总结<锁>——(全局锁&行级锁&表级锁)【共享锁,排他锁】【间隙锁,临键锁】【表锁,元数据锁,意向锁】
MySQL 锁
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
用户9615083
2022/12/25
1.4K0
MySQL 锁
【MySQL】一文带你理清<表级锁>(表锁,元数据锁,意向锁)
YY的秘密代码小屋
2024/09/09
4020
【MySQL】一文带你理清<表级锁>(表锁,元数据锁,意向锁)
10人面试9个答错?鹅厂T12详解MySQL加锁机制
👉腾小云导读 鹅厂有一道关于「数据库锁」的面试题。我们发现其实很多 DBA (数据库管理员,Database administrator)包括工作好几年的 DBA 都答得不太好。这说明 MySQL 锁的机制其实还是比较复杂,值得深入研究。本文对3条简单的查询语句加锁情况进行分析,以期帮助各位开发者彻底搞清楚加锁细节。欢迎阅读~ 👉看目录,点收藏 1 MySQL 有哪些锁?    1.1 全局锁    1.2 表锁    1.3 行锁 2 锁的兼容情况 3 锁信息查看方式 4 测试环境搭建    4.1 建立
腾讯云开发者
2023/05/29
6131
10人面试9个答错?鹅厂T12详解MySQL加锁机制
一文理解MySQL的锁机制与死锁排查
MySQL的并发控制是在数据安全性和并发处理能力之间的权衡,通过不同的锁策略来决定对系统开销和性能的影响。
全菜工程师小辉
2021/06/25
2.7K0
一文理解MySQL的锁机制与死锁排查
MySQL高级9-锁
  锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除了传统的计算资源(CPU、RAM、i/O)的挣用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性,有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素,从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
Se7eN_HOU
2023/09/06
2820
MySQL高级9-锁
【MySQL】MySQL锁(二)表锁与行锁测试
上篇文章我们简单的了解了一大堆锁相关的概念,然后只是简单的演示了一下 InnoDB 和 MyISAM 之间 表锁 与 行锁 的差别。相信大家还是意犹未尽的,今天我们就来用代码说话,实际地操作一下,看看如何进行手动的加 表锁 与 行锁 ,并进行一些相关的实验测试。
硬核项目经理
2024/04/18
8840
【MySQL】MySQL锁(二)表锁与行锁测试
万字硬核实战分析MySQL死锁
本文先完整介绍MySQL的各种锁类型及加锁机制,之后通过一个案例带大家了解如何分析排查死锁问题。最后,再介绍几种预防死锁的方法。以下是示例表的表结构
会玩code
2022/04/24
9940
万字硬核实战分析MySQL死锁
对于MySQL你必须要了解的锁知识
MySQL 的锁按照范围可以分为全局锁、表锁、行锁,其中行锁是由数据库引擎实现的,并不是所有的引擎都提供行锁,MyISAM 就不支持行锁,所以文章介绍行锁会以InnoDB引擎为例来介绍行锁。
本人秃顶程序员
2019/04/29
7360
对于MySQL你必须要了解的锁知识
MySQL锁系列
全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
AiDBA宝典
2023/04/26
3110
MySQL锁系列
⑩⑦【MySQL】锁:全局锁、表级锁、行级锁
①表锁 :表共享读锁(read lock) / 表独享写锁(write lock)
.29.
2023/11/21
8360
⑩⑦【MySQL】锁:全局锁、表级锁、行级锁
mysql数据库的各种锁分析
为了防止在事务中出现表结构操作,导致事务无法保证前后一致性问题,mysql增加了 (meta data lock,MDL) 锁.
仙士可
2021/10/28
1.8K0
mysql数据库的各种锁分析
MySQL十三:小一万字+14张图读懂锁机制
MySQL中的锁有很多种,各种锁应用在不同的地方。「MySQL依靠锁机制可以让多个事务更新一行数据的时候串行化」。
云扬四海
2022/09/26
4690
MySQL 中的锁机制
事务要读取对象 ,必须先获得共享锁,这样防止幻读。事务要修改对象,必须先获得独占锁,这样防止脏写。
真正的飞鱼
2022/09/15
9510
细说MySQL锁机制:S锁、X锁、意向锁…
好久没有深入地写文章了,这次来发一篇,通过mysql事物 | Joseph's Blog (gitee.io)和其他一些博客有感进行一些补充,InnoDB详解在下期发布
Karos
2023/06/14
9K0
细说MySQL锁机制:S锁、X锁、意向锁…
SQL锁总结
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
海盗船长
2023/10/11
2660
SQL锁总结
推荐阅读
相关推荐
MySql中的锁
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档