全面掌握Solidity智能合约开发

2024年11月25日更新 208 人订阅
原价: ¥ 26 限时优惠
专栏简介 比特币区块结构Merkle树及简单支付验证分析 非对称加密技术- RSA算法数学原理分析 智能合约语言 Solidity 教程系列2 - 地址类型介绍 剖析非同质化代币ERC721-全面解析ERC721标准 搞懂 Solidity 事件Event - 如何在DApp中使用 以太坊扩容 之 分片(Sharding) 智能合约语言 Solidity 教程系列12 - 库的使用 深入理解Plasma(四)Plasma Cash 技术工坊42期 - 区块链子链技术及墨客链的实现方案 脱颖而出 | 成都链安入选『腾讯区块链加速器全球32强』 参与Eth2 Staking系列(2) - 激励篇 应用案例之黄金资产证券化Digix白皮书和DGD解读 全面理解智能合约升级 测试文章123456 简析主流匿名币:Dash、门罗币、Zcash、SERO 以太坊ERC1155协议分析及代码测试 Hyperledger Fabric 1.4 Kafka分布式环境搭建 跟我学 Solidity :开发环境 跟我学 Solidity:关于变量 跟我学 Solidity : 变量的存储 跟我学 Solidity :引用变量 跟我学 Solidity :函数 跟我学 Solidity :合约的创建和继承 跟我学 Solidity :工厂模式 用Web3.js构建第一个Dapp 跟我学Solidity:事件 Solidity 中 immutable (不可变量)与constant(常量) [译] Solidity 0.6.x更新:继承 解析 Solidity 0.6 新引入的 try/catch 特性 探究新的 Solidity 0.8 版本 探索以太坊合约委托调用(DelegateCall) 停止使用Solidity的transfer() 使用工厂提高智能合约安全性 Solidity 怎样写出最节省Gas的智能合约[译] Solidity 优化 - 编写 O(1) 复杂度的可迭代映射 Solidity 优化 - 控制 gas 成本 Solidity 优化 - 减少智能合约的 gas 消耗的8种方法 Solidity 优化 - 如何维护排序列表 Solidity 优化:打包变量优化 gas 在 Solidity中使用值数组以降低 gas 消耗 Solidity 技巧:如何减少字节码大小及节省 gas 计算Solidity 函数的Gas 消耗 "Stack Too Deep(堆栈太深)" 解决方案 合约实践:避免区块Gas限制导致问题 如何缩减合约以规避合约大小限制 Solidity 类特性 安全的处理 ERC20 转账(解决非标准 ERC20 问题) Solidity 十大常见安全问题 [译]更好Solidity合约调试工具: console.log 智能合约开发的最佳实践 - 强烈推荐

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

解决非标准 ERC20 问题

你可能认为在 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这样的 token 来说这段代码是很完美的,调用 transfer 函数并在出错的时候回退调用。

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

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 (代码)实现中这样做,也是我建议这样做的原因。

而对于返回值的做法,是有争议的。如果你知道正在交易的 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

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 ,但是它的有些行为却不像,有些出现缺少返回值的错误

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

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

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是这样做的

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来实现。

这是一个围绕 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 赞助支持。

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

1 条评论

请先 登录 后评论