场景介绍
这是一个刚接触波场网络的合约开发者,经常碰到的问题。波场网络的 USDT 信息如下:
- USDT Token 地址:
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
- 合约代码 : https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t/code
这个错误场景的描述:使用的库是:@openzeppelin/contracts
中的 IERC20
和 SafeERC20
。用 using SafeERC20 for IERC20;
将 SafeERC20 内的方法赋给 IERC20。此时 IERC20 已经拥有了 SafeERC20 上的 safeTransferFrom
和 safeTransfer
方法。
✅ 用户向合约储存 USDT 时候是可以正常完成的:
- 向合约 approve(授权) USDT 的操作权。
- 合约内使用
safeTransferFrom
方法,将钱从用户的地址拿到指定的地方。
❌ 但是用户取款时候报错 失败-交易REVERT
/ FAILED -TRANSACTION REVERT
或者收到 SafeERC20: ERC20 operation did not succeed
错误,波场项目方给出的提示是
导致交易Revert的原因可能如下:
1.调用throw
2.如果你调用require的参数(表达式)最终结果为false
3.如果你通过消息调用某个函数,但该函数没有正确结束
4.如果你使用new关键字创建合约,但合约没有正确创建
5.如果你的合约通过一个没有payable修饰符的公有函数接收TRX
6.transfer() 失败 // ❌ 此时属于这个错误
7.调用revert()
8.到达最大函数栈深64
9.了解更多请前往:https://developers.tron.network/docs/exception-handling
错误总结
造成这种事情的原因是波场 USDT 的 transfer 函数没有遵守代码规范,灾难由它产生。
波场的 USDT 主合约 TetherToken.transfer
函数继承的父合约的 transfer,。但是父合约的 transfer 虽然声明了返回值,但却没有真正的做返回,因此会导致主合约的 transfer 会永久返回 false
。从而导致在使用 safeTransfer 调用 USDT 的 transfer 时永远都只返回 false,或许会导致可以存 USDT,但是无法提 USDT。
提醒:在波场上部署 USDT 相关合约的时候,需要针对 USDT 做适配,做好充足的测试再推出。
合约分析
这个代币是基于 pragma solidity ^0.4.18;
开发的,使用 solidity 0.4.25
编译,合约依赖关系如下。
文件 1(共 10 个):BasicToken.sol
文件 8(共 10 个):StandardToken.sol
文件 9(共 10 个):StandardTokenWithFees.sol
文件 10(共 10 个):TetherToken.sol
下面逐个分析
第一、TetherToken.sol
以太坊网络:核心代码
transfer
和 transferFrom
方法均没有return
标记。transfer 函数虽然有返回值,但是函数声明时却没有声明,导致后续没有返回值。核心代码如下:
contract TetherToken is Pausable, StandardToken, BlackList {
// Forward ERC20 methods to upgraded contract if this one is deprecated
function transfer(address _to, uint _value) public whenNotPaused {
require(!isBlackListed[msg.sender]);
if (deprecated) {
return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value);
} else {
return super.transfer(_to, _value);
}
}
// Forward ERC20 methods to upgraded contract if this one is deprecated
function transferFrom(address _from, address _to, uint _value) public whenNotPaused {
require(!isBlackListed[_from]);
if (deprecated) {
return UpgradedStandardToken(upgradedAddress)
.transferFromByLegacy(msg.sender, _from, _to, _value);
} else {
return super.transferFrom(_from, _to, _value);
}
}
}
以太坊上的代码可以在浏览器那查看到: https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code
波场网络:核心代码
transfer
和 transferFrom
方法均有 returns
标记。主合约中的 transfer 函数继承的父合约的 transfer。但是父合约的 transfer 虽然有声明返回值,但是函数内部却没有写 return
,因此会导致主合约的 transfer 会永久返回 false。
contract TetherToken is Pausable, StandardTokenWithFees, BlackList {
// Forward ERC20 methods to upgraded contract if this one is deprecated
function transfer(address _to, uint _value) public whenNotPaused returns (bool) {
require(!isBlackListed[msg.sender]);
if (deprecated) {
return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value);
} else {
return super.transfer(_to, _value);
}
}
// Forward ERC20 methods to upgraded contract if this one is deprecated
function transferFrom(address _from, address _to, uint _value) public whenNotPaused
returns (bool) {
require(!isBlackListed[_from]);
if (deprecated) {
return UpgradedStandardToken(upgradedAddress)
.transferFromByLegacy(msg.sender, _from, _to, _value);
} else {
return super.transferFrom(_from, _to, _value);
}
}
}
上面代码 return super.transfer(_to, _value);
会将 super.transfer()
函数的运行结果返回出去,运行 StandardTokenWithFees.transfer
。
波场上的代码可以在浏览器那查看到: https://tronscan.org/#/token20/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t/code
第二、StandardTokenWithFees.sol
StandardTokenWithFees.transfer
也确实标记了 returns (bool)
。但是函数内部只写了 super.transfer
却没有写 retuen super.transfer
。此函数的返回值永远是 false
。
contract StandardTokenWithFees is StandardToken, Ownable {
// ❌ 函数内部没有 return;
function transfer(address _to, uint _value) public returns (bool) {
uint fee = calcFee(_value);
uint sendAmount = _value.sub(fee);
super.transfer(_to, sendAmount);
if (fee > 0) {
super.transfer(owner, fee);
}
}
// ✅ 函数内部做了 return true;
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
uint fee = calcFee(_value);
uint sendAmount = _value.sub(fee);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (allowed[_from][msg.sender] < MAX_UINT) {
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
}
Transfer(_from, _to, sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(_from, owner, fee);
}
return true;
}
}
我们忽略上面的没有写 retrun
的操作,接着再寻找父级 super.transfer
,会运行 StandardToken.transfer
第三、StandardToken.sol
由代码 contract StandardToken is ERC20, BasicToken
得知,此时会运行 BasicToken.transfer
。
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 is ERC20Basic {
function allowance(address owner, address spender) public view returns (uint256);
function transferFrom(address from, address to, uint256 value) public returns (bool);
function approve(address spender, uint256 value) public returns (bool);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract StandardToken is ERC20, BasicToken {
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
Transfer(_from, _to, _value);
return true;
}
}
第四、BasicToken.sol
接着查看这个 BasicToken.transfer
,发现这里确实写了 return true
。
/**
* @title ERC20Basic
* @dev Simpler version of ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/179
*/
contract ERC20Basic {
function totalSupply() public constant returns (uint);
function balanceOf(address who) public view returns (uint256);
function transfer(address to, uint256 value) public returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
}
/**
* @title Basic token
* @dev Basic version of StandardToken, with no allowances.
*/
contract BasicToken is ERC20Basic {
/**
* @dev transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);
// SafeMath.sub will throw if there is not enough balance.
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
Transfer(msg.sender, _to, _value);
return true;
}
}
所以 StandardTokenWithFees.transfer
内部的 super.transfer(owner, fee);
是会收到 true 的。但是在函数内没有对外做 return
。
USDT 的合约开发者大概率是想写 return super.transfer();
这样的风格。但是写代码的时候却忘记了这件事情。
为什么要使用 SafeTransfer
关于 ERC20 的 transfer 是否需要返回值。按照标准来说并不做要求,规范是 function transfer(address to, uint value) external;
没有 returns (bool success)
因为规范没有要求,所以主流分为两种操作方法
- transfer 函数默认表示交易成功。如果遇到失败,ERC20 合约内部做 revert ,直接回滚交易。
- transfer 函数通过返回值表示交易状态,而不是暴力的直接 revert 交易。由调用合约内部灵活处理该部分的逻辑。返回
false
值代表交易失败,返回true
表示交易成功。
这些都是符合 ERC20 标准的,所以 @openzeppelin/contracts
使用 SafeERC20 进行兼容。核心代码如下
library SafeERC20 {
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
function _callOptionalReturn(IERC20 token, bytes memory data) private {
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) { // Return data is optional
// solhint-disable-next-line max-line-length
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
上面代码是如果没有返回值,则直接完成,如果交易失败,Token 内部做 revert 会导致交易失败。但是如果有返回值,就使用 bool 解析返回值,返回值必须为 true
,否则报错 SafeERC20: ERC20 operation did not succeed
。这个兼容处理是没有任何问题的。
但是 USDT 没有按照约定走,它的代码逻辑是想对外抛出返回值,但是开发者自己忘记返回了。所以合约永远返回 false
。
该问题的解决办法
使用默认 transfer 即可。如果拥有很多资金池模型的合约,可以通过属性来判断,比如引入 erc20IsStandard
变量,来判断否为标准的 ERC20 合约。
if (erc20IsStandard) {
IERC20(erc20Token).safeTransfer(_to_user, _amount);
} else {
IERC20(erc20Token).transfer(_to_user, _amount);
}
开发者的反思
需要严格做好单元测试和 E2E 测试,很多开发者有写单元测试,但是几乎不做生产环境的 E2E 测试。如果合约内部牵扯到其他合约或者 ERC20 资产,一定要做好代码的 review。