WTF-Solidity 第 33 讲深度解析:智能合约 ERC20 空投合约(Airdrop Contract)源码级实战
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
本篇文章基于 WTF-Solidity 极简入门教程第 33 讲(English 版文档)展开。空投(Airdrop)是加密行业中项目方将代币免费发放给特定用户群体的营销策略,而利用智能合约一次交易批量向大量地址发送ERC20代币,是显著提升空投效率的关键做法。读完本文,你将掌握Airdrop空投合约的完整设计思路、getSum/multiTransferToken/multiTransferETH三个核心函数的实现细节、approve+transferFrom授权转账机制的底层原理,并能在 Remix 中完成一次完整的 ERC20 代币空投实战,同时了解空投场景中常见的 DoS 安全风险与防御手段。
一、空投(Airdrop)与智能合约批量转账
空投是币圈一种常见的营销策略:项目方将代币免费发放给特定用户群体。为了获得空投资格,用户通常需要完成一些简单任务,如测试产品、分享新闻、推荐朋友等。项目方通过空投获得种子用户,用户则获得一笔财富,是双赢的局面。
由于一次空投往往涉及成百上千个接收地址,项目方不可能一笔一笔地手动转账。借助智能合约,在一笔交易内通过循环将ERC20代币批量发送给多个地址,可以显著提高空投效率——这正是本讲Airdrop合约的核心价值。对应源码位于 33_Airdrop/Airdrop.sol(英文版见 Languages/en/33_Airdrop_en/Airdrop.sol)。
二、空投合约核心函数设计
Airdrop合约的逻辑非常简单:利用for循环,一笔交易把ERC20代币发送给多个地址。合约核心包含三个函数:数组求和的getSum()、发送 ERC20 空投的multiTransferToken()以及发送 ETH 空投的multiTransferETH()。
2.1 getSum():计算空投总量
getSum()是纯函数(pure),对传入的uint256数组求和,用于计算一次空投的代币总量,供后续授权额度校验使用:
// sum function for arrays function getSum(uint256[] calldata _arr) public pure returns(uint sum) { for(uint i = 0; i < _arr.length; i++) sum = sum + _arr[i]; }实现细节要点:
- 使用
calldata修饰数组参数,避免将数据从 calldata 复制到 memory 的开销,降低 gas 消耗; - 声明为
public pure,不读取也不修改状态变量,可被外部直接调用,也可被合约内部函数复用(源码位置)。
2.2 multiTransferToken():发送 ERC20 代币空投
该函数负责向多个地址批量转账ERC20代币,共 3 个参数:
| 参数 | 类型 | 含义 |
|---|---|---|
_token | address | 要空投的 ERC20 代币合约地址 |
_addresses | address[] | 接收空投的用户地址数组 |
_amounts | uint256[] | 空投数量数组,与_addresses中每个地址一一对应 |
/// @notice Transfer ERC20 tokens to multiple addresses, authorization is required before use /// /// @param _token The address of ERC20 token for transfer /// @param _addresses The array of airdrop addresses /// @param _amounts The array of amount of tokens (airdrop amount for each address) function multiTransferToken( address _token, address[] calldata _addresses, uint256[] calldata _amounts ) external { // Check: The length of _addresses array should be equal to the length of _amounts array require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); IERC20 token = IERC20(_token); // Declare IERC contract variable uint _amountSum = getSum(_amounts); // Calculate the total amount of airdropped tokens // Check: The authorized amount of tokens should be greater than or equal to the total amount of airdropped tokens require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); // for loop, use transferFrom function to send airdrops for (uint8 i; i < _addresses.length; i++) { token.transferFrom(msg.sender, _addresses[i], _amounts[i]); } }函数体包含两道关键检查(require):
- 数组长度一致性检查:
_addresses.length == _amounts.length。保证每个空投地址都有对应的数量,避免数组错位导致的逻辑错误; - 授权额度检查:
token.allowance(msg.sender, address(this)) >= _amountSum。_amountSum由getSum(_amounts)计算得出。因为合约最终调用的是transferFrom,代币从msg.sender账户扣除,所以空投发起人必须先调用代币合约的approve()授权给Airdrop合约,且授权额度必须不小于空投总量。
检查通过后,进入for循环,逐一调用token.transferFrom(msg.sender, _addresses[i], _amounts[i])完成批量转账(源码位置)。
2.3 multiTransferETH():发送 ETH 空投
除了 ERC20 代币,合约也支持批量发送原生 ETH,共 2 个参数:
| 参数 | 类型 | 含义 |
|---|---|---|
_addresses | address payable[] | 接收空投的用户地址数组(payable表示可接收 ETH) |
_amounts | uint256[] | 空投数量数组,与地址一一对应 |
/// Transfer ETH to multiple addresses function multiTransferETH( address payable[] calldata _addresses, uint256[] calldata _amounts ) public payable { // Check: _addresses and _amounts arrays should have the same length require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); // Calculate total amount of ETH to be airdropped uint _amountSum = getSum(_amounts); // Check: transferred ETH should equal total amount require(msg.value == _amountSum, "Transfer amount error"); // Use a for loop to transfer ETH using transfer function for (uint256 i = 0; i < _addresses.length; i++) { _addresses[i].transfer(_amounts[i]); } }与代币空投不同,ETH 空投是payable函数,空投的 ETH 由调用者通过msg.value随交易一起转入合约,因此校验逻辑变为:msg.value == _amountSum(英文版源码)。
三、源码级原理剖析:授权转账机制与安全增强
3.1 IERC20 接口与 allowance 授权机制
multiTransferToken中使用IERC20 token = IERC20(_token)将地址转换为接口引用,再调用接口方法。IERC20接口定义了 ERC20 标准的全部必要函数与事件,见 33_Airdrop/IERC20.sol:
function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool);Airdrop合约同文件下还附带了一个简化版ERC20实现(33_Airdrop/Airdrop.sol#L81-L144),其底层逻辑可以清晰印证整个授权-转账闭环:
approve():设置allowance[msg.sender][spender] = amount,即“所有者”授权“调用者”可代为花费的代币额度,并释放Approval事件;transferFrom():先从allowance[sender][msg.sender]中扣减授权额度,再从balanceOf[sender]中扣减余额、给recipient增加余额,并释放Transfer事件。
因此,整个空投流程的资金走向是:空投发起人(msg.sender)→ approve 授权 → Airdrop 合约调用 transferFrom → 各接收地址。Airdrop合约自身并不持有代币,只是“代扣代发”的中转者,这正是空投合约不需要预先锁仓资金的原因。若读者想深入了解 ERC20 标准,可继续学习本仓库 31_ERC20 一讲。
3.2 从源码结构看中文版合约的安全增强
需要特别说明:本仓库的 中文版 Airdrop.sol 相比英文版额外做了 DoS 安全加固(英文版文档对应的 Languages/en/33_Airdrop_en/Airdrop.sol 仍使用transfer循环转账)。从源码结构看,中文版合约引入了三个增强点:
mapping(address => uint) failTransferList; // 记录转账失败的地址与金额 for (uint256 i = 0; i < _addresses.length; i++) { // 注释代码有Dos攻击风险, 并且transfer 也是不推荐写法 // _addresses[i].transfer(_amounts[i]); (bool success, ) = _addresses[i].call{value: _amounts[i]}(""); if (!success) { failTransferList[_addresses[i]] = _amounts[i]; } } // 给空投失败提供主动操作机会 function withdrawFromFailList(address _to) public { uint failAmount = failTransferList[msg.sender]; require(failAmount > 0, "You are not in failed list"); failTransferList[msg.sender] = 0; (bool success, ) = _to.call{value: failAmount}(""); require(success, "Fail withdraw"); }- 改用底层
call代替transfer:transfer固定只转发 2300 gas,且目标地址的回调一旦失败会整体回滚;而call允许返回成功布尔值,便于逐笔处理失败(源码位置); - 失败清单
failTransferList:单笔转账失败时记录地址与金额,不让整个循环回滚,避免“一颗老鼠屎坏了一锅粥”; withdrawFromFailList()主动领取:失败的接收方可以自行调用该函数领回属于自己的金额,符合“pull 优于 push”的安全实践(源码位置)。
3.3 为什么循环转账存在 DoS 风险
智能合约的循环批量转账是典型的 DoS(拒绝服务)漏洞温床。若用transfer/强制require(success)的方式逐笔推送,任何一个恶意合约地址在fallback中revert,都会让整笔空投交易失败,资金被卡在合约中。这与本仓库 S09_DoS 合约安全专题 中 Akutar 项目因退款循环中被恶意合约卡死、上万个 ETH 被永久锁仓的案例同源。因此空投合约的推荐做法是:单笔失败不阻断整体,并让用户自行领取(pull)而非批量推送(push)。
四、Remix 实操:完成一次 ERC20 代币空投
下面在 Remix IDE 中完整走一遍空投流程(5 个步骤),使用的合约即 Airdrop.sol(内含Airdrop与简化版ERC20两个合约)。
步骤 1:部署 ERC20 代币合约,并给自己 mint 10000 单位代币。
在编译环境中分别选择ERC20合约进行部署,构造函数填入名称WTF与符号WTF:
部署成功后调用mint函数,amount填入10000,为自己铸造 1 万枚代币:
步骤 2:部署 Airdrop 空投合约。
在合约下拉列表中选择Airdrop合约并部署,得到空投合约地址:
步骤 3:调用 ERC20 合约的approve(),给 Airdrop 合约授权 10000 单位代币。
spender填入空投合约地址,amount填入10000。这一步是第 2.2 节中allowance检查能够通过的前提:
步骤 4:调用 Airdrop 合约的multiTransferToken()执行空投。
参数按如下填写(以 Remix 自带测试账号为例):
_token:填入 ERC20 代币合约地址;_addresses:接收空投的用户地址数组:
["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"]_amounts:对应的空投数量数组:
[100, 200]步骤 5:调用 ERC20 合约的balanceOf()验证空投结果。
查询上述用户地址的代币余额,若分别变为100和200,说明空投成功:
五、空投合约实践要点与注意事项
- 授权先于空投:
multiTransferToken依赖approve+transferFrom的授权转账机制,发起空投前必须先授权且额度充足,否则第二个require会以"Need Approve ERC20 token"报错回滚; - 数组长度必须一致:
_addresses与_amounts长度不一致会直接回滚("Lengths of Addresses and Amounts NOT EQUAL"),这是防止空投金额错位的第一道防线; - ETH 空投需精确配平:
multiTransferETH要求msg.value与_amountSum完全相等,多余或不足都会回滚; - 警惕循环转账 DoS:接收地址中若混入恶意合约(
fallback中revert),强制推送式转账会让整笔空投失败;应像中文版源码那样使用call+ 失败清单,并支持失败方主动领取; - gas 与成本控制:空投地址越多、单笔交易 gas 越高,需要权衡每批空投的地址数量,必要时分批执行。
六、总结
本讲介绍了如何使用 Solidity 编写ERC20代币空投合约:通过getSum()计算空投总量,借助multiTransferToken()与multiTransferETH()两个循环转账函数,一次交易即可向大量地址批量分发代币或 ETH,极大提升空投效率;同时深入剖析了IERC20接口下的allowance/approve/transferFrom授权转账原理,以及循环转账场景下 DoS 漏洞的成因与失败清单、主动领取等防御方案。从部署代币合约、铸造、授权到执行空投、验证余额,完整闭环均可在 Remix 中复现,可作为后续接入 Merkle 白名单空投、NFT 空投等更复杂方案(参考 36_MerkleTree)的基础。
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考