在智能合约中处理转账,最常见是 Ether 和 ERC20Token 的转账。
在转账 Ether 时,一般我们直接使用 <address payable>.transfer(uint256)(或者 send) 函数,但是这个函数有个限制,只能使用 2300 gas,而且不能调整,这样就会出现一个问题,如果在合约内转给另一个合约地址,合约内对 fallback 方法做了额外的操作,这样消耗的 gas 就增加,进而交易会被 revert。
如下所示,如果使用转到下面这个合约,额外带有一个 event 会有额外的 gas 消耗,那么 transfer(send) 固定的 2300 gas 限制一定会失败。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | pragma solidity ^0.7.0;
 contract Wallet {
 event Deposit( address indexed sender,uint256 indexed amount );
 
 receive() payable external {
 emit Deposit(msg.sender, msg.value);
 }
 }
 
 | 
我们可以使用下面函数就行封装,使用 call 方法并指定 value 即可,这样就可以事先调用 rpc 的 eth_estimateGas 接口来动态调整 gas limit。
| 12
 3
 4
 
 | function safeTransferETH(address to, uint value) internal {(bool success,) = to.call{value:value}(new bytes(0));
 require(success, 'ETH_TRANSFER_FAILED');
 }
 
 | 
对于ERC20 的转账,一般我们会使用接口将地址转化合约对象方式来直接调用转账方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | pragma solidity ^0.7.0;
 interface ERC20 {
 function transfer(address to, uint256 tokens) external returns (bool success);
 }
 
 contract Manager {
 function ERC20Transfer(
 Token _token,
 address _to,
 uint256 _value
 ) public returns (bool) {
 return _token.transfer(_to, _value);
 }
 }
 
 | 
但这有个问题,部分ERC20合约是使用了没有返回值非标准的函数接口,但 Solidity 编译器会把函数调用的返回值进行转换成接口定义的 bool 值,非标准合约调用会造成 revert。
在旧版本的 ^0.4.22 版本的 solidity 可以使用下面方式检查,代码来源自sec-bit/badERC20Fix
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
 | function isContract(address addr) internal {assembly {
 if iszero(extcodesize(addr)) { revert(0, 0) }
 }
 }
 
 function handleReturnData() internal returns (bool result) {
 assembly {
 switch returndatasize()
 case 0 { // not a std erc20
 result := 1
 }
 case 32 { // std erc20
 returndatacopy(0, 0, 32)
 result := mload(0)
 }
 default { // anything else, should revert for safety
 revert(0, 0)
 }
 }
 }
 
 function asmTransfer(address _erc20Addr, address _to, uint256 _value) internal returns (bool result) {
 // Must be a contract addr first!
 isContract(_erc20Addr);
 // call return false when something wrong
 require(_erc20Addr.call(bytes4(keccak256("transfer(address,uint256)")), _to, _value));
 // handle returndata
 return handleReturnData();
 }
 
 | 
当时的 address.call 方法调用只返回是否调用成功,所以需要加入很多汇编代码。但现在 address.call 方法除了返回是否调用成功外,还有了调用返回值。
| 1
 | <address>.call(bytes memory) returns (bool, bytes memory)
 | 
所以我们可以更加方便的进行数据判断:
| 12
 3
 4
 5
 6
 
 | function safeTransferERC20(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));
 // 如果返回值 data 不为空,那么解码为 bool 并判断是否为 true
 require(success && (data.length == 0 || abi.decode(data, (bool))), 'ERC20_TRANSFER_FAILED');
 }
 
 | 
对于 ERC20 这种调用方法,还需要注意一点,低级调用 call 方法,如果地址不是合约那么也会返回 true,所以这里为了安全,最好先保证地址为合约地址。
| 12
 3
 4
 5
 6
 7
 8
 
 | modifier OnlyContract(address token) {assembly {
 if iszero(extcodesize(token)) {
 revert(0, 0)
 }
 }
 _;
 }
 
 | 
其它 ERC20 方法,类如 transferFrom 以及 approve 方法都可以同上面方式处理。
需要注意的是,这种处理 ERC20 的几个辅助函数也不是万能的,例如下面代码,transfer 调用了 _transfer() 方法,但是并没有返回值,所以始终会返回 false。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | function _transfer(address _from ,address _to, uint256 _value) internal returns (bool) {require(_to != address(0));
 require(_value <= _balances[msg.sender]);
 
 _balances[_from] -= _value;
 _balances[_to] += _value;
 emit Transfer(_from, _to, _value);
 return true;
 }
 
 function transfer(address to, uint value) public returns (bool) {
 _transfer(msg.sender, to, value);
 }
 
 | 
这种不规范的合约还有很多,最简单的处理方式是单独处理特殊合约。