区块链系列许久没有更新了 , 这里更新一篇关于 ERC20 智能合约批量转账(空投)的文章 .

在以太坊开发中 , 免不了会有批量转账(空投)的需求 . 因为 nonce 机制的问题 , 转账的形式明显不智 , 此时我们需要通过智能合约实现 . 通常空投合约我们是在网上找一份 , 然后 Tether USDT 首当其冲成为我们的测试 Token .

在两者合约部署后 , 测试时很可能发现无法空投成功 , 得到 Gas estimation failed...gas required exceeds allowance... 的错误(当然有先 approve 成功) . 于是乎我们去空投合约各种定位问题 , 最终可能也很难发现问题 . 其实这里并不是空投合约自身产生了问题 , 而是一个 Token 合约兼容性的问题 . 接下来我们进入分析 .

过程分析

首先是先 copy 一份 Tether USDT 的合约源码进行部署 , 这里我们选择 Rinkeby 测试网络 , 过程略过 .

然后我们再部署空投合约 .

笔者最初是在以下空投合约基础上选择后进行了修改 , 然后部署 .

两者均有演示网站 , 最后笔者选择了 multisender

multisender 合约有两个空投方法

  • multisendEther(address[] _contributors, uint256[] _balances) 空投 ETH
  • multisendToken(address token, address[] _contributors, uint256[] _balances) 空投 ERC20 Token

而在测试中发现 , 空投 Token 的方法无法成功 , 报 ``Gas estimation failed…gas required exceeds allowance… 出现这个错误的原因较大的可能是合约中require()` 断言条件 , 或者直接触发了其他失败的断言 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function multisendToken(address token, address[] _contributors, uint256[] _balances) public hasFee payable {
if (token == 0x000000000000000000000000000000000000bEEF){
multisendEther(_contributors, _balances);
} else {
uint256 total = 0;
require(_contributors.length <= arrayLimit());
ERC20 erc20token = ERC20(token);
uint8 i = 0;
for (i; i < _contributors.length; i++) {
erc20token.transferFrom(msg.sender, _contributors[i], _balances[i]);
total += _balances[i];
}
setTxCount(msg.sender, txCount(msg.sender).add(1));
emit Multisended(total, token);
}
}

这里贴一下合约此函数的完整实现 , 测试中基本上显式断言都是满足的 , 这里就只可能是 erc20token.transferFrom(...) 触发了失败 .

经过一番定位发现 , 根本原因是 Tether USDT 未严格遵循 ERC20 标准 , transferFrom 方法未返回 bool 值 , 而 solidity 编译器在 0.4.22 版本之后对于这种情况会发生 revert.

而对于 0.4.22 之前的版本 , 当外部合约调用没有返回值的 transfertransferFrom , 外部合约还是会在内存中查询函数的返回值 . 由于没有真正的返回值 , 外部调用合约返回值本应在内存中存储对应的位置查找 , 将查到的数据作为返回值 . 实际上 , 查询到的返回值并不是函数的返回值 , 通常是一个大于 0 的值 , 外部调用合约将其视为 true .

因为根据 EVM 的内存布局, 函数调用数据(call data)与函数返回数据(return data)是共同使用一块内存区域, 如果 transfer() 函数没有调用 RETURN 指令返回任何值, 那么如果调用者去用 RETURNDATACOPY 来取返回值的时候, 会将内存中的脏数据取回. 脏数据的值很大概率会为 非零值, 这在EVM里面代表 “true”. 但是这里请注意, 脏数据也很有可能是 , 代表 “false”. 这就意味着: 即使token合约正常完成了转账, 但是却返回 “false”, 导致外部的 DAPP 误认为转账没有成功, 进而可能引发安全漏洞.

这里建议查看 数千份以太坊 Token 合约不兼容问题浮出水面, 严重影响 DAPP 生态 , 文章有详细的说明 .

解决方案

对于这种不兼容 , 我们该如何应对 ?

  1. Token 方重新发布合约
  2. DAPP 开发者需要同一个安全的调用代码来访问各种不兼容的 ERC20 Token 合约
  3. 以太坊硬分叉升级

虽然我们认为第一种方案是能彻底解决这一部兼容性问题带来的安全风险 , 但是可行性不高 .第三种近期难以得到妥善解决 .

最终只能从方案二开始着手 , 我们需要采用下面的代码片段作为调用 transfertranferFrom 等的中间层代码 :

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
library ERC20AsmTransfer {    
function asmTransfer(address _erc20Addr, address _to, uint256 _value) internal returns (bool result) {

// Must be a contract addr first!
assembly {
if iszero(extcodesize(_erc20Addr)) { revert(0, 0) }
}

// call return false when something wrong
require(_erc20Addr.call(bytes4(keccak256("transfer(address,uint256)")), _to, _value));

// handle returndata
assembly {
switch returndatasize()
case 0 { // not a std erc20
result := not(0)
}
case 32 { // std erc20
returndatacopy(0, 0, 32)
result := mload(0)
}
default { // anything else, should revert for safety
revert(0, 0)
}
}
return result;
}
}

这段代码使用 call 方法手动直接调用 transfer()函数, 并使用内联 assembly code 手动获取 returndatasize() 进行判断. 如果为 0, 则表明被调用 ERC20 合约正常执行完毕, 但没有返回值, 即转账成功;如果为 32, 则表明ERC20合约符合标准, 直接进行 returndatacopy() 操作, 调用 mload() 拿到返回值进行判断即可;如果为其他值则 revert. 封装成函数替代 transfer 使用.

完整的中间层代码可以参考 SECBIT 实验室的仓库 badERC20Fix

另外这里附上修改后的 multisender 源码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
pragma solidity ^0.4.24;

library ERC20AsmFn {

function isContract(address addr) internal view {
assembly {
if iszero(extcodesize(addr)) { revert(0, 0) }
}
}

function handleReturnData() internal pure 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();
}

function asmTransferFrom(address _erc20Addr, address _from, 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("transferFrom(address,address,uint256)")), _from, _to, _value));

// handle returndata
return handleReturnData();
}

function asmApprove(address _erc20Addr, address _spender, 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("approve(address,uint256)")), _spender, _value));

// handle returndata
return handleReturnData();
}
}


interface ERC20 {
function balanceOf(address who) external view returns (uint256);
function transfer(address _to, uint256 _value) external returns (bool success);
function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
function approve(address _spender, uint256 _value) external returns (bool success);
}

contract TestERC20AsmTransfer {

using ERC20AsmFn for ERC20;

function dexTestTransfer(address _token, address _to, uint256 _value) public {
require(ERC20(_token).asmTransfer(_to, _value));
}

function dexTestTransferFrom(address _token, address _from, address _to, uint256 _value) public {
require(ERC20(_token).asmTransferFrom(_from, _to, _value));
}

function dexTestApprove(address _token, address _spender, uint256 _value) public {
require(ERC20(_token).asmApprove(_spender, _value));
}
}




// File: contracts/EternalStorage.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;


/**
* @title EternalStorage
* @dev This contract holds all the necessary state variables to carry out the storage of any contract.
*/
contract EternalStorage {

mapping(bytes32 => uint256) internal uintStorage;
mapping(bytes32 => string) internal stringStorage;
mapping(bytes32 => address) internal addressStorage;
mapping(bytes32 => bytes) internal bytesStorage;
mapping(bytes32 => bool) internal boolStorage;
mapping(bytes32 => int256) internal intStorage;

}

// File: contracts/UpgradeabilityOwnerStorage.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;


/**
* @title UpgradeabilityOwnerStorage
* @dev This contract keeps track of the upgradeability owner
*/
contract UpgradeabilityOwnerStorage {
// Owner of the contract
address private _upgradeabilityOwner;

/**
* @dev Tells the address of the owner
* @return the address of the owner
*/
function upgradeabilityOwner() public view returns (address) {
return _upgradeabilityOwner;
}

/**
* @dev Sets the address of the owner
*/
function setUpgradeabilityOwner(address newUpgradeabilityOwner) internal {
_upgradeabilityOwner = newUpgradeabilityOwner;
}

}

// File: contracts/UpgradeabilityStorage.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;


/**
* @title UpgradeabilityStorage
* @dev This contract holds all the necessary state variables to support the upgrade functionality
*/
contract UpgradeabilityStorage {
// Version name of the current implementation
string internal _version;

// Address of the current implementation
address internal _implementation;

/**
* @dev Tells the version name of the current implementation
* @return string representing the name of the current version
*/
function version() public view returns (string) {
return _version;
}

/**
* @dev Tells the address of the current implementation
* @return address of the current implementation
*/
function implementation() public view returns (address) {
return _implementation;
}
}

// File: contracts/OwnedUpgradeabilityStorage.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity >=0.4.24;





/**
* @title OwnedUpgradeabilityStorage
* @dev This is the storage necessary to perform upgradeable contracts.
* This means, required state variables for upgradeability purpose and eternal storage per se.
*/
contract OwnedUpgradeabilityStorage is UpgradeabilityOwnerStorage, UpgradeabilityStorage, EternalStorage {}

// File: contracts/SafeMath.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;


/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {

/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}

/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}

/**
* @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}

/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}

// File: contracts/multisender/Ownable.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;



/**
* @title Ownable
* @dev This contract has an owner address providing basic authorization control
*/
contract Ownable is EternalStorage {
/**
* @dev Event to show ownership has been transferred
* @param previousOwner representing the address of the previous owner
* @param newOwner representing the address of the new owner
*/
event OwnershipTransferred(address previousOwner, address newOwner);

/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner());
_;
}

/**
* @dev Tells the address of the owner
* @return the address of the owner
*/
function owner() public view returns (address) {
return addressStorage[keccak256("owner")];
}

/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner the address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
setOwner(newOwner);
}

/**
* @dev Sets a new owner address
*/
function setOwner(address newOwner) internal {
emit OwnershipTransferred(owner(), newOwner);
addressStorage[keccak256("owner")] = newOwner;
}
}

// File: contracts/multisender/Claimable.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;


/**
* @title Claimable
* @dev Extension for the Ownable contract, where the ownership needs to be claimed.
* This allows the new owner to accept the transfer.
*/
contract Claimable is EternalStorage, Ownable {
function pendingOwner() public view returns (address) {
return addressStorage[keccak256("pendingOwner")];
}

/**
* @dev Modifier throws if called by any account other than the pendingOwner.
*/
modifier onlyPendingOwner() {
require(msg.sender == pendingOwner());
_;
}

/**
* @dev Allows the current owner to set the pendingOwner address.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
addressStorage[keccak256("pendingOwner")] = newOwner;
}

/**
* @dev Allows the pendingOwner address to finalize the transfer.
*/
function claimOwnership() public onlyPendingOwner {
emit OwnershipTransferred(owner(), pendingOwner());
addressStorage[keccak256("owner")] = addressStorage[keccak256("pendingOwner")];
addressStorage[keccak256("pendingOwner")] = address(0);
}
}

// File: contracts/multisender/UpgradebleStormSender.sol

// Roman Storm Multi Sender
// To Use this Dapp: https://rstormsf.github.io/multisender
pragma solidity 0.4.24;

contract UpgradebleStormSender is OwnedUpgradeabilityStorage, Claimable {
using SafeMath for uint256;
using ERC20AsmFn for ERC20;

event Multisended(uint256 total, address tokenAddress);
event ClaimedTokens(address token, address owner, uint256 balance);

modifier hasFee() {
if (currentFee(msg.sender) > 0) {
require(msg.value >= currentFee(msg.sender));
}
_;
}

function() public payable {}

function initialize(address _owner) public {
require(!initialized());
setOwner(_owner);
setArrayLimit(200);
setDiscountStep(0.00005 ether);
setFee(0.05 ether);
boolStorage[keccak256("rs_multisender_initialized")] = true;
}

function initialized() public view returns (bool) {
return boolStorage[keccak256("rs_multisender_initialized")];
}

function txCount(address customer) public view returns(uint256) {
return uintStorage[keccak256("txCount", customer)];
}

function arrayLimit() public view returns(uint256) {
return uintStorage[keccak256("arrayLimit")];
}

function setArrayLimit(uint256 _newLimit) public onlyOwner {
require(_newLimit != 0);
uintStorage[keccak256("arrayLimit")] = _newLimit;
}

function discountStep() public view returns(uint256) {
return uintStorage[keccak256("discountStep")];
}

function setDiscountStep(uint256 _newStep) public onlyOwner {
require(_newStep != 0);
uintStorage[keccak256("discountStep")] = _newStep;
}

function fee() public view returns(uint256) {
return uintStorage[keccak256("fee")];
}

function currentFee(address _customer) public view returns(uint256) {
if (fee() > discountRate(msg.sender)) {
return fee().sub(discountRate(_customer));
} else {
return 0;
}
}

function setFee(uint256 _newStep) public onlyOwner {
require(_newStep != 0);
uintStorage[keccak256("fee")] = _newStep;
}

function discountRate(address _customer) public view returns(uint256) {
uint256 count = txCount(_customer);
return count.mul(discountStep());
}

function multisendToken(address token, address[] _contributors, uint256[] _balances) public hasFee payable {
if (token == 0x000000000000000000000000000000000000bEEF){
multisendEther(_contributors, _balances);
} else {
uint256 total = 0;
require(_contributors.length <= arrayLimit());
ERC20 erc20token = ERC20(token);
uint8 i = 0;
for (i; i < _contributors.length; i++) {
require(erc20token.asmTransferFrom(msg.sender,_contributors[i],_balances[i]));
total += _balances[i];
}
setTxCount(msg.sender, txCount(msg.sender).add(1));
emit Multisended(total, token);
}
}

function multisendEther(address[] _contributors, uint256[] _balances) public payable {
uint256 total = msg.value;
uint256 current = currentFee(msg.sender);
require(total >= current);
require(_contributors.length <= arrayLimit());
total = total.sub(current);
uint256 i = 0;
for (i; i < _contributors.length; i++) {
require(total >= _balances[i]);
total = total.sub(_balances[i]);
_contributors[i].transfer(_balances[i]);
}
setTxCount(msg.sender, txCount(msg.sender).add(1));
emit Multisended(msg.value, 0x000000000000000000000000000000000000bEEF);
}

function claimTokens(address _token) public onlyOwner {
if (_token == 0x0) {
owner().transfer(address(this).balance);
return;
}
ERC20 erc20token = ERC20(_token);
uint256 balance = erc20token.balanceOf(this);
require(erc20token.asmTransfer(owner(), balance));
emit ClaimedTokens(_token, owner(), balance);
}

function setTxCount(address customer, uint256 _txCount) private {
uintStorage[keccak256("txCount", customer)] = _txCount;
}

}

参考链接