WTF-Solidity 实战:手写一个 ERC20 代币水龙头(Token Faucet)合约并完成领币全流程
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
本篇技术指南以 WTF-Solidity 仓库第 32 讲(Languages/pt-br/32_Faucet/readme.md)为核心,讲解代币水龙头(Token Faucet)的由来,以及如何用 Solidity 实现一个简版 ERC20 水龙头合约——用户可免费领取固定数量代币且每个地址仅限一次。读完本文,你将掌握水龙头合约的状态变量、事件与函数设计思路,能够基于 Faucet.sol 的源码逐行理解领币逻辑,并能在 Remix 中独立完成"部署代币 → 部署水龙头 → 充值 → 领币 → 查余额"的完整实操闭环。
1. 代币水龙头是什么:从比特币水龙头说起
当人渴了会去水龙头接水,当用户想要免费代币时,就会去"代币水龙头"领取。所谓代币水龙头(Token Faucet),就是让用户免费领取代币的网站或应用,通常用于新项目的冷启动、测试网代币分发或社区拉新。
最早的代币水龙头是比特币(BTC)水龙头。按照教程文档的记载:现在一枚 BTC 的价格已达数万美元量级,但在 2010 年,BTC 的价格不足 $0.1,持有人极少。为了扩大影响力,比特币社区的 Gavin Andresen 开发了 BTC 水龙头,让人们可以免费领取 BTC。撸羊毛人人都喜欢,大量用户因此涌入,其中一部分最终成为了 BTC 的忠实信徒。BTC 水龙头累计发放了超过 19,700 枚 BTC,按文档记录当时价值约 6 亿美元。
这个历史案例揭示了一个朴素的产品逻辑:免费代币是吸引新用户的低成本获客工具。ERC20 水龙头正是这一思路在智能合约层面的标准实现。
2. ERC20 水龙头合约的整体设计
本讲实现的是一个简版ERC20水龙头,逻辑非常直白:
- 预先将一些 ERC20 代币转入水龙头合约地址;
- 用户调用
requestTokens()函数领取100单位代币; - 每个地址只能领取一次。
在仓库中,该合约有两个位置可供对照:
- 葡萄牙语版 Languages/pt-br/32_Faucet/Faucet.sol:从源码结构看,这是一个填空练习模板——
Faucet合约的函数体被保留为注释,供学习者自行补全实现; - 中文完整版 32_Faucet/Faucet.sol:包含可编译运行的完整实现,本文的代码拆解均以此为准。
值得注意的是,32_Faucet/Faucet.sol 这一个文件里同时定义了ERC20(完整代币合约)和Faucet(水龙头合约)两个合约。水龙头依赖上一讲 31_ERC20 中介绍的 ERC20 标准,并通过接口合约 32_Faucet/IERC20.sol 与代币交互,而不是耦合某个具体实现。
水龙头合约本身只包含:3 个状态变量 + 1 个事件 + 2 个函数(构造函数与requestTokens())。
3. 状态变量与事件:水龙头的"库存与台账"
3.1 三个状态变量
水龙头合约定义了 3 个状态变量,分别控制"每次发多少""发什么币""谁领过":
uint256 public amountAllowed = 100; // 每次领取 100 单位代币 address public tokenContract; // token 合约地址 mapping(address => bool) public requestedAddress; // 记录领取过代币的地址| 变量 | 类型 | 作用 | 说明 |
|---|---|---|---|
amountAllowed | uint256 | 每次可领取的代币数量 | 默认100;注意单位不是"枚",而是代币的最小原子单位wei。由于代币存在小数位数(decimals)划分,实际到手的代币枚数可能远小于 100 |
tokenContract | address | 发放的 ERC20 代币合约地址 | 部署时由构造函数写入,之后不可更改(该简版无 setter) |
requestedAddress | mapping(address => bool) | 已领取过代币的地址台账 | 实现"每个地址只能领一次"的防重领机制 |
这里特别强调amountAllowed的单位问题:示例中的 ERC20 合约将decimals设为18(见 32_Faucet/Faucet.sol 中uint8 public decimals = 18;),因此100单位意味着100 wei,即10^-16枚代币,数量极小。这也是教程在演示中铸造/转账10000单位的原因——水龙头领取数量在实际产品中通常需要按代币精度重新折算。
3.2 SendToken 事件
合约定义了一个SendToken事件,在每次成功领币时释放,记录领取者地址与领取数量,方便链上索引与前端监听:
// SendToken 事件 event SendToken(address indexed Receiver, uint256 indexed Amount);两个参数都标记了indexed,意味着它们可以作为事件的检索过滤条件(Topic),例如前端可通过SendToken事件实时展示"谁领了多少代币"。
4. 核心函数实现:构造函数与 requestTokens
4.1 构造函数:绑定要发放的代币
水龙头需要知道"发什么代币",因此在部署时通过构造函数传入 ERC20 代币合约地址:
// 部署时设定 ERC20 代币合约 constructor(address _tokenContract) { tokenContract = _tokenContract; // 写入 token 合约地址 }构造函数将_tokenContract写入状态变量tokenContract。后续requestTokens()中的所有代币操作(查余额、转账)都以该地址为准。
4.2 requestTokens():领币主函数
requestTokens()是水龙头的核心函数,任何用户都可以调用它来领取代币:
// 用户领取代币函数 function requestTokens() external { require(!requestedAddress[msg.sender], "Can't Request Multiple Times!"); // 每个地址只能领一次 IERC20 token = IERC20(tokenContract); // 创建 IERC20 合约对象 require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // 水龙头空了 token.transfer(msg.sender, amountAllowed); // 发送 token requestedAddress[msg.sender] = true; // 记录领取地址 emit SendToken(msg.sender, amountAllowed); // 释放 SendToken 事件 }逐行拆解,该函数依次完成 5 个动作:
- 防重领校验:
require(!requestedAddress[msg.sender], ...)检查调用者是否已领过;已领过则直接回滚交易,错误信息为 "Can't Request Multiple Times!"。 - 接口对象化:
IERC20 token = IERC20(tokenContract);把状态变量里的地址包装成IERC20接口对象,之后即可调用balanceOf、transfer等标准方法。接口定义见 32_Faucet/IERC20.sol,它声明了totalSupply、balanceOf、transfer、allowance、approve、transferFrom六个函数以及Transfer、Approval两个事件。 - 库存校验:
require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!")检查水龙头合约自身的代币余额是否足够;不足则回滚,错误信息为 "Faucet Empty!"。这一步在转账前完成,避免转入资金不足导致后续失败。 - 转账与记账:
token.transfer(msg.sender, amountAllowed)把代币发给调用者;随后requestedAddress[msg.sender] = true将该地址标记为"已领取",封堵重复领取。 - 事件通知:
emit SendToken(msg.sender, amountAllowed)释放事件,供链下索引。
4.3 与同文件 ERC20 实现的配合
水龙头能够运行,前提是代币合约真正实现了IERC20接口。在 32_Faucet/Faucet.sol 中,同文件的ERC20合约提供了:
transfer(address recipient, uint amount):直接操作balanceOf映射完成转账并释放Transfer事件;mint(uint amount):向调用者铸造代币,更新totalSupply,并释放Transfer(address(0), msg.sender, amount);burn(uint amount):销毁调用者持有的代币;balanceOf/allowance等查询函数。
水龙头通过接口而非具体合约名调用这些方法,意味着任何正确实现IERC20的 ERC20 代币(包括 OpenZeppelin 标准实现)都能直接接入该水龙头,无需修改水龙头代码——这是面向接口编程在合约协作中的典型体现。
5. 安全边界与设计要点
从源码结构可以总结出这个简版水龙头的几个设计要点与边界:
- 防重领:
requestedAddress映射 +require前置校验,从状态层面杜绝"一个地址反复撸"。 - 余额预检:先查
balanceOf(address(this))再转账,避免对空水龙头的无效调用。 - 0.8.x 安全特性:合约使用
pragma solidity ^0.8.34,Solidity 0.8 起默认启用算术溢出检查;示例中transfer内部的balanceOf[msg.sender] -= amount若出现余额不足会直接回滚,不会产生负余额。 - 无管理员权限:该简版水龙头没有 owner、没有暂停开关、没有限时机制,任何地址均可直接调用
requestTokens()领取。从代码结构看,若用于生产环境,可在此基础上扩展管理员配置、领取频率限制、总量上限、人机验证等能力(这些属于推断的扩展方向,并非本合约已有功能)。 - 事件可检索:
SendToken的两个参数均为indexed,链下可据此高效过滤领币记录。
6. Remix 实操演示:五分钟跑通领币全流程
以下步骤基于 Remix IDE 在线环境,完整演示从部署代币到领取成功的全流程(对应教程文档 Languages/pt-br/32_Faucet/readme.md 的演示章节):
Step 1:部署 ERC20 代币合约
编译 32_Faucet/Faucet.sol,在部署面板选择ERC20合约,构造函数参数填写名称与符号WTF(name/symbol均填 "WTF"),点击 Deploy。部署成功后,在ERC20合约实例上调用mint,给自己铸造10000单位代币。
Step 2:部署 Faucet 水龙头合约
在部署面板切换为Faucet合约,构造函数参数_tokenContract填入上一步ERC20合约的地址,点击 Deploy。
Step 3:为水龙头充值
回到ERC20合约实例,调用transfer(address recipient, uint256 amount),将recipient填为Faucet合约地址、amount填10000,把代币转入水龙头。至此水龙头"库存"就位。
Step 4:切换账户,调用 requestTokens() 领币
在 Remix 右上角切换一个新账户,调用Faucet合约的requestTokens()函数。交易成功后,可在终端/日志面板看到SendToken事件被释放,Amount为100:
Step 5:用 balanceOf 验证领取结果
回到ERC20合约实例,调用balanceOf(address)查询刚才领币账户的余额,输出显示为100,领取成功:
若再用同一账户第二次调用requestTokens(),交易会因 "Can't Request Multiple Times!" 被回滚——这正是防重领机制的直观验证。
7. 总结
本讲围绕 WTF-Solidity 第 32 讲完成了三件事:首先回顾了代币水龙头的起源(比特币水龙头及其历史意义),然后逐字段拆解了 ERC20 水龙头合约的完整实现——3 个状态变量、1 个SendToken事件、构造函数与requestTokens()领币函数,最后通过 Remix 五步实操验证了"部署代币 → 部署水龙头 → 充值 → 领币 → 查余额"的完整闭环。
这套模式的核心价值在于:以最小代码量实现"免费代币 + 防重领 + 事件可追踪"的分发能力,是空投、测试网代币分发、冷启动拉新等场景的经典基础组件。下一篇可以在此基础上进一步探索更复杂的分发方案(如 Merkle 树白名单空投等,参见仓库后续章节)。
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考