在智能合约中处理转账,最常见是 Ether 和 ERC20Token 的转账。
在转账 Ether 时,一般我们直接使用 <address payable>.transfer(uint256)
(或者 send) 函数,但是这个函数有个限制,只能使用 2300 gas,而且不能调整,这样就会出现一个问题,如果在合约内转给另一个合约地址,合约内对 fallback 方法做了额外的操作,这样消耗的 gas 就增加,进而交易会被 revert。
如下所示,如果使用转到下面这个合约,额外带有一个 event 会有额外的 gas 消耗,那么 transfer(send) 固定的 2300 gas 限制一定会失败。
1 2 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。
1 2 3 4
| function safeTransferETH(address to, uint value) internal { (bool success,) = to.call{value:value}(new bytes(0)); require(success, 'ETH_TRANSFER_FAILED'); }
|
对于ERC20 的转账,一般我们会使用接口将地址转化合约对象方式来直接调用转账方法。
1 2 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
1 2 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)
|
所以我们可以更加方便的进行数据判断:
1 2 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,所以这里为了安全,最好先保证地址为合约地址。
1 2 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。
1 2 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); }
|
这种不规范的合约还有很多,最简单的处理方式是单独处理特殊合约。