使用foundry编写、测试和部署水龙头合约
- 安装foundry
在foundry的发布页,找到linux版本 https://github.com/foundry-rs/foundry/releases
通过github proxy加速,下载压缩包后解压,添加环境变量,生效,成功
$ wget https://tvv.tw/https://github.com/foundry-rs/foundry/releases/download/nightly-e6de72c999bf8b72c166c5c37fbf71f5a7893cef/foundry_nightly_linux_amd64.tar.gz
正在保存至: “foundry_nightly_linux_amd64.tar.gz”
foundry_nightly_linux_amd64 100%[==========================================>] 66.96M 11.3MB/s 用时 7.8s
2025-06-21 14:19:02 (8.54 MB/s) - 已保存 “foundry_nightly_linux_amd64.tar.gz” [70213620/70213620])
$ gzip -d foundry_nightly_linux_amd64.tar.gz
$ tar -xvf foundry_nightly_linux_amd64.tar -C foundry
forge
cast
anvil
chisel
$ nano .bashrc
$ source .bashrc
$ forge -V
forge 1.2.3-nightly (e6de72c999 2025-06-21T06:02:18.449873636Z)
$ cast -V
cast 1.2.3-nightly (e6de72c999 2025-06-21T06:02:18.449873636Z)
- 安装vscode
$ wget https://vscode.download.prss.microsoft.com/dbazure/download/stable/18e3a1ec544e6907be1e944a94c496e302073435/code_1.101.1-1750254731_amd64.deb
正在保存至: “code_1.101.1-1750254731_amd64.deb”
code_1.101.1-1750254731_amd64 100%[===============================================>] 103.57M 11.3MB/s 用时 9.7s
2025-06-21 14:25:52 (10.7 MB/s) - 已保存 “code_1.101.1-1750254731_amd64.deb” [108605322/108605322])
vscode设置字体,安装插件
solidity + javascript + Prettier Code formatter增强代码易读性
- 从零构建Web3代币水龙头Dapp
作者的github https://github.com/Luboy23/
按照视频教程演示步骤,和github的源代码,操作
创建项目文件夹02faucet,再创建合约文件夹contracts,当前文件夹下打开vscode
初始化foundry项目,比较快
$ forge init
Installed forge-std v1.9.7
Initialized forge project
删除3个自带的文件scripts/Counter.s.sol 和 src/Counter.sol 和 test/Counter.t.sol
src文件夹下两个合约LuLuCoin.sol 和 LLCFaucet.sol
LuLuCoin.sol是第一课编写并测试通过的合约,直接复制粘贴源代码
// SPDX-License-Identifier: MIT
// 许可证声明:指定合约的版权许可为 MIT,允许代码的自由使用和修改。
pragma solidity ^0.8.20;
// 指定 Solidity 编译器的版本。这里选择的是 0.8.20,确保合约在这一版本下编译通过。
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// 从 OpenZeppelin 导入 ERC20 合约,提供标准的 ERC20 代币功能。
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
// 从 OpenZeppelin 导入 Ownable 合约,确保只有合约所有者可以执行特定操作。
contract LuLuCoin is ERC20, Ownable {
// 声明 LuLuCoin 合约,继承 ERC20 和 Ownable 两个功能模块。
event Mint(uint256 indexed amount);
// 声明 Mint 事件:在代币铸造时记录铸造的数量。
event Burn(uint256 indexed amount);
// 声明 Burn 事件:在代币销毁时记录销毁的数量。
string public _name = "LuLuCoin";
// 代币名称,供外部调用查询。
string public _symbol = "LLC";
// 代币符号,供外部调用查询。
constructor(address initialOwner) ERC20(_name, _symbol) Ownable(initialOwner) {
// 构造函数:初始化合约时设置代币的名称、符号,以及指定初始所有者。
}
function mint(uint256 _amount) public onlyOwner {
// mint 函数:允许合约所有者铸造代币。
// 参数 _amount:铸造的代币数量。
_mint(msg.sender, _amount);
// 调用 OpenZeppelin 提供的 _mint 函数,将指定数量的代币铸造到调用者账户。
emit Mint(_amount);
// 触发 Mint 事件,记录铸造的数量。
}
function burn(uint256 _amount) public onlyOwner {
// burn 函数:允许合约所有者销毁代币。
// 参数 _amount:销毁的代币数量。
_burn(msg.sender, _amount);
// 调用 OpenZeppelin 提供的 _burn 函数,从调用者账户销毁指定数量的代币。
emit Burn(_amount);
// 触发 Burn 事件,记录销毁的数量。
}
}
安装OpenZeppelin
$ forge install OpenZeppelin/openzeppelin-contracts
Installing openzeppelin-contracts in /home/zhangyu/Web3Tutorial/02faucet/lib/openzeppelin-contracts (url: Some
致命错误:无法访问 'https://github.com/OpenZeppelin/openzeppelin-contracts/':Recv failure: 连接被对方重设致命错误:无法克隆 'https://github.com/OpenZeppelin/openzeppelin-contracts' 到子模组路径
Error: git submodule exited with code 128
仍然是git被重设无法访问。参考这个文章
【foundry】安装依赖 以openzeppelin为例
https://blog.csdn.net/qq2942713658/article/details/139045827
在.gitmodules文件中,手动添加
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
并通过Github Proxy加速 github.akams.cn
替换该url地址 https://tvv.tw/https://github.com/OpenZeppelin/openzeppelin-contracts
保存,再安装,成功
$ forge install OpenZeppelin/openzeppelin-contracts
Installed openzeppelin-contracts v5.3.0
在foundry.toml文件添加重映射
remapping = ["@openzeppelin/contracts/=lib/openzeppelin-contracts/"]
此时重新打开LuLuCoin.sol文件,编译器就不再红线报错
新建LLCFaucet.sol文件,导入3个库IERC20+Ownable+SafeERC20,直接复制粘贴源代码
// SPDX-License-Identifier: MIT
// 许可证声明:指定合约的版权许可为 MIT,允许代码的自由使用和修改。
pragma solidity ^0.8.20;
// 指定 Solidity 编译器的版本。这里选择的是 0.8.20,确保合约在这一版本下编译通过。
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// 导入 OpenZeppelin 的 IERC20 接口,用于操作 ERC20 标准的代币。
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
// 导入 OpenZeppelin 的 Ownable 合约,用于提供所有者控制权限。
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// 导入 OpenZeppelin 的 SafeERC20 库,用于安全地进行 ERC20 代币的转账操作。
contract LLCFaucet is Ownable {
using SafeERC20 for IERC20;
// 使用 SafeERC20 库来增强 ERC20 代币的安全性。
IERC20 public token;
// 定义一个 IERC20 类型的变量 token,用于存储要发放的代币合约。
address public tokenAddress;
// 定义一个地址类型的变量 tokenAddress,用于存储代币合约的地址。
uint256 public dripInterval;
// 定义一个 uint256 类型的变量 dripInterval,用于设置用户请求代币的最小间隔时间。
uint256 public dripLimit;
// 定义一个 uint256 类型的变量 dripLimit,用于设置用户每次最多可以领取的代币数量。
mapping(address => uint256) dripTime;
// 定义一个映射,将用户地址映射到他们上次领取代币的时间戳。
error LLCFaucet__IntervalHasNotPassed();
// 定义一个错误类型,当用户请求代币的间隔时间未过时抛出此错误。
error LLCFaucet__ExceedLimit();
// 定义一个错误类型,当用户请求领取的代币数量超过最大限制时抛出此错误。
error LLCFaucet__FaucetEmpty();
// 定义一个错误类型,当水龙头中没有足够代币时抛出此错误。
error LLCFaucet__InvalidAmount();
// 定义一个错误类型,当存入的代币数量无效时抛出此错误。
event LLCFaucet__Drip(address indexed Receiver, uint256 indexed Amount);
// 定义一个事件,当用户成功领取代币时触发,记录接收者地址和领取的代币数量。
event LLCFaucet__OwnerDeposit(uint256 indexed amount);
// 定义一个事件,当合约所有者存入代币时触发,记录存入的代币数量。
constructor(address _tokenAddress, uint256 _dripInterval, uint256 _dripLimit, address _owner) Ownable(_owner) {
// 合约构造函数,初始化代币合约地址、领取间隔、领取限制以及合约所有者地址。
tokenAddress = _tokenAddress;
dripInterval = _dripInterval;
dripLimit = _dripLimit;
token = IERC20(_tokenAddress);
// 使用传入的代币地址初始化 token 变量为 ERC20 代币合约的实例。
}
function drip(uint256 _amount) external {
// 定义 drip 函数,允许用户请求领取代币。
uint256 targetAmount = _amount;
// 设置目标领取的代币数量。
if (block.timestamp < dripTime[_msgSender()] + dripInterval) {
// 如果当前时间戳小于用户上次领取代币时间加上领取间隔,则抛出错误。
revert LLCFaucet__IntervalHasNotPassed();
}
if (targetAmount > dripLimit) {
// 如果请求的代币数量大于最大领取限制,则抛出错误。
revert LLCFaucet__ExceedLimit();
}
if (token.balanceOf(address(this)) < targetAmount) {
// 如果合约中代币余额不足以满足请求的代币数量,则抛出错误。
revert LLCFaucet__FaucetEmpty();
}
dripTime[_msgSender()] = block.timestamp;
// 更新用户的上次领取代币时间为当前时间戳。
token.safeTransfer(_msgSender(), targetAmount);
// 安全地将目标数量的代币转账到请求者地址。
emit LLCFaucet__Drip(_msgSender(), targetAmount);
// 触发事件,记录领取代币的用户和领取数量。
}
function deposit(uint256 _amount) external onlyOwner {
// 定义 deposit 函数,允许合约所有者存入代币。
if (_amount > token.balanceOf(_msgSender())) {
// 如果合约所有者存入的代币数量大于其账户余额,则抛出错误。
revert LLCFaucet__InvalidAmount();
}
token.safeTransferFrom(_msgSender(), address(this), _amount);
// 安全地从合约所有者地址转账代币到合约地址。
emit LLCFaucet__OwnerDeposit(_amount);
// 触发事件,记录存入的代币数量。
}
function setDripInterval(uint256 _newDripInterval) public onlyOwner {
// 定义 setDripInterval 函数,允许合约所有者设置新的领取间隔。
dripInterval = _newDripInterval;
}
function setDripLimit(uint256 _newDripLimit) public onlyOwner {
// 定义 setDripLimit 函数,允许合约所有者设置新的领取限制。
dripLimit = _newDripLimit;
}
function setTokenAddress(address _newTokenAddress) public onlyOwner {
// 定义 setTokenAddress 函数,允许合约所有者设置新的代币合约地址。
tokenAddress = _newTokenAddress;
}
function getDripTime(address _user) external view returns (uint256) {
// 定义 getDripTime 函数,返回指定用户的上次领取代币时间。
return dripTime[_user];
}
function getDripInterval() external view returns (uint256) {
// 定义 getDripInterval 函数,返回当前的领取间隔。
return dripInterval;
}
function getDripLimit() external view returns (uint256) {
// 定义 getDripLimit 函数,返回当前的领取限制。
return dripLimit;
}
}
两个合约文件都编译通过
$ forge compile
[⠊] Compiling...
[⠒] Compiling 13 files with Solc 0.8.30
[⠑] Installing Solc version 0.8.30
[⠢] Successfully installed Solc 0.8.30
[⠆] Solc 0.8.30 finished in 4.96s
Compiler run successful!
编写测试用例
编写测试合约LLCFauctTest.t.sol,导入forge-std测试库 和 LuLuCoin合约 和 LLCFaucet合约
直接复制粘贴源代码
// SPDX-License-Identifier: MIT
// 许可证声明:指定合约的版权许可为 MIT,允许代码的自由使用和修改。
pragma solidity ^0.8.20;
// 指定 Solidity 编译器的版本。这里选择的是 0.8.20,确保合约在这一版本下编译通过。
import "forge-std/Test.sol";
// 导入 forge-std 测试库,提供测试框架和工具。
import {LuLuCoin} from "../src/LuLuCoin.sol";
// 导入 LuLuCoin 合约,用于测试代币的相关操作。
import {LLCFaucet} from "../src/LLCFaucet.sol";
// 导入 LLCFaucet 合约,用于测试水龙头发放代币的功能。
contract LLCFaucetTest is Test {
// 定义 LLCFaucetTest 测试合约,继承 Test 合约以便使用 forge-std 提供的测试工具。
LuLuCoin public llc;
// 定义一个公共的 LuLuCoin 类型的变量 llc,用于测试中的代币合约实例。
LLCFaucet public faucet;
// 定义一个公共的 LLCFaucet 类型的变量 faucet,用于测试中的水龙头合约实例。
address owner = vm.addr(1);
// 定义一个变量 owner,表示合约所有者的地址。
address user = vm.addr(2);
// 定义一个变量 user,表示测试中的用户地址。
uint256 dripInterval = 10 seconds;
// 定义一个领取间隔为 10 秒,用于控制用户请求代币的最小间隔时间。
uint256 public dripLimit = 100;
// 定义一个领取限制为 100,表示用户每次最多可以领取 100 个代币。
modifier ownerDeposit() {
// 定义一个修饰器 ownerDeposit,用于模拟合约所有者存入代币并等待领取间隔。
vm.startPrank(owner);
// 模拟合约所有者的操作。
llc.approve(address(faucet), 1_000);
// 合约所有者授权水龙头合约转移 1,000 个代币。
faucet.deposit(1_000);
// 合约所有者向水龙头存入 1,000 个代币。
vm.stopPrank();
// 停止模拟合约所有者的操作。
vm.warp(block.timestamp + dripInterval);
// 模拟时间流逝,确保时间超过领取间隔。
_;
}
function setUp() public {
// 定义 setUp 函数,初始化测试环境。
llc = new LuLuCoin(owner);
// 创建一个新的 LuLuCoin 合约实例,指定合约所有者。
faucet = new LLCFaucet(address(llc), dripInterval, dripLimit, owner);
// 创建一个新的 LLCFaucet 合约实例,指定代币合约地址、领取间隔、领取限制和合约所有者。
vm.deal(owner, 1_000 ether);
// 给合约所有者分配 1,000 个以太币。
vm.deal(user, 1_000 ether);
// 给测试中的用户分配 1,000 个以太币。
vm.prank(owner);
// 模拟合约所有者的操作。
llc.mint(1_000);
// 合约所有者铸造 1,000 个代币。
}
function testSuccessIfOwnerSetDripLimit() public {
// 定义测试函数 testSuccessIfOwnerSetDripLimit,测试合约所有者是否能成功设置领取限制。
uint256 newLimit = 200;
// 定义一个新的领取限制为 200。
vm.startPrank(owner);
// 模拟合约所有者的操作。
faucet.setDripLimit(newLimit);
// 合约所有者设置新的领取限制为 200。
vm.stopPrank();
// 停止模拟合约所有者的操作。
assertEq(newLimit, faucet.getDripLimit());
// 断言新的领取限制已经成功设置。
}
function testSuccessIfOwnerSetDripInterval() public {
// 定义测试函数 testSuccessIfOwnerSetDripInterval,测试合约所有者是否能成功设置领取间隔。
uint256 newInterval = 20 seconds;
// 定义一个新的领取间隔为 20 秒。
vm.startPrank(owner);
// 模拟合约所有者的操作。
faucet.setDripInterval(newInterval);
// 合约所有者设置新的领取间隔为 20 秒。
vm.stopPrank();
// 停止模拟合约所有者的操作。
assertEq(newInterval, faucet.getDripInterval());
// 断言新的领取间隔已经成功设置。
}
function testSuccessIfOwnerSetTokenAddress() public {
// 定义测试函数 testSuccessIfOwnerSetTokenAddress,测试合约所有者是否能成功设置代币地址。
address newTokenAddress = vm.addr(3);
// 定义一个新的代币地址。
vm.startPrank(owner);
// 模拟合约所有者的操作。
faucet.setTokenAddress(newTokenAddress);
// 合约所有者设置新的代币地址。
vm.stopPrank();
// 停止模拟合约所有者的操作。
assertEq(newTokenAddress, faucet.tokenAddress());
// 断言新的代币地址已经成功设置。
}
function testSuccessIfOwnerDeposit() public {
// 定义测试函数 testSuccessIfOwnerDeposit,测试合约所有者是否能成功存入代币。
vm.startPrank(owner);
// 模拟合约所有者的操作。
llc.approve(address(faucet), 1_000);
// 合约所有者授权水龙头合约转移 1,000 个代币。
faucet.deposit(1_000);
// 合约所有者向水龙头存入 1,000 个代币。
vm.stopPrank();
// 停止模拟合约所有者的操作。
assertEq(llc.balanceOf(address(faucet)), 1_000);
// 断言水龙头合约已成功接收 1,000 个代币。
}
function testSuccessIfUserDrip() public ownerDeposit {
// 定义测试函数 testSuccessIfUserDrip,测试用户是否能成功领取代币。
vm.prank(user);
// 模拟用户的操作。
faucet.drip(1);
// 用户请求领取 1 个代币。
assertEq(llc.balanceOf(user), 1);
// 断言用户已经成功领取了 1 个代币。
}
function testRevertIfTimeHasNotPassed() public {
// 定义测试函数 testRevertIfTimeHasNotPassed,测试如果时间未过,则应 revert。
vm.startPrank(owner);
// 模拟合约所有者的操作。
llc.approve(address(faucet), 1_000);
// 合约所有者授权水龙头合约转移 1,000 个代币。
faucet.deposit(1_000);
// 合约所有者向水龙头存入 1,000 个代币。
vm.stopPrank();
// 停止模拟合约所有者的操作。
vm.prank(user);
// 模拟用户的操作。
vm.expectRevert();
// 预期会发生 revert。
faucet.drip(1);
// 用户请求领取 1 个代币,但由于时间未过,应 revert。
}
function testRevertIfAmountLimit() public ownerDeposit{
// 定义测试函数 testRevertIfAmountLimit,测试如果请求的代币数量超过限制应 revert。
vm.prank(user);
// 模拟用户的操作。
vm.expectRevert();
// 预期会发生 revert。
faucet.drip(101);
// 用户请求领取 101 个代币,超过了领取限制,应 revert。
}
function testRevertIfFaucetEmpty() public ownerDeposit {
// 定义测试函数 testRevertIfFaucetEmpty,测试如果水龙头代币不足应 revert。
vm.startPrank(owner);
// 模拟合约所有者的操作。
faucet.setDripLimit(2_000);
// 合约所有者设置领取限制为 2,000。
vm.stopPrank();
// 停止模拟合约所有者的操作。
vm.prank(user);
// 模拟用户的操作。
faucet.drip(1_000);
// 用户请求领取 1,000 个代币。
assertEq(1_000, llc.balanceOf(user));
// 断言用户已领取 1,000 个代币。
assertEq(0, llc.balanceOf(address(llc)));
// 断言 LuLuCoin 合约余额为 0。
vm.warp(block.timestamp + dripInterval);
// 模拟时间流逝,确保时间超过领取间隔。
vm.expectRevert();
// 预期会发生 revert。
faucet.drip(1);
// 用户再次请求领取 1 个代币,但水龙头已空,应 revert。
}
function testDripTimeRightAfterUserDrip() public ownerDeposit {
// 定义测试函数 testDripTimeRightAfterUserDrip,测试用户领取代币后领取时间是否正确。
vm.prank(user);
// 模拟用户的操作。
faucet.drip(1);
// 用户请求领取 1 个代币。
assertEq(block.timestamp, faucet.getDripTime(user));
// 断言用户的领取时间应该是当前时间。
}
}
setUp()函数相当于构造函数,初始化变量 所有文件测试通过
$ forge test
[⠒] Compiling...
No files changed, compilation skipped
Ran 9 tests for test/LLCFauctTest.t.sol:LLCFaucetTest
[PASS] testDripTimeRightAfterUserDrip() (gas: 123610)
[PASS] testRevertIfAmountLimit() (gas: 72601)
[PASS] testRevertIfFaucetEmpty() (gas: 127876)
[PASS] testRevertIfTimeHasNotPassed() (gas: 68673)
[PASS] testSuccessIfOwnerDeposit() (gas: 63721)
[PASS] testSuccessIfOwnerSetDripInterval() (gas: 20304)
[PASS] testSuccessIfOwnerSetDripLimit() (gas: 20239)
[PASS] testSuccessIfOwnerSetTokenAddress() (gas: 21574)
[PASS] testSuccessIfUserDrip() (gas: 123706)
Suite result: ok. 9 passed; 0 failed; 0 skipped; finished in 23.62ms (7.60ms CPU time)
Ran 1 test suite in 105.44ms (23.62ms CPU time): 9 tests passed, 0 failed, 0 skipped (9 total tests)
测试覆盖率
$ forge coverage
╭-------------------+----------------+----------------+---------------+----------------╮
| File | % Lines | % Statements | % Branches | % Funcs |
+======================================================================================+
| src/LLCFaucet.sol | 96.97% (32/33) | 96.30% (26/27) | 75.00% (3/4) | 100.00% (9/9) |
|-------------------+----------------+----------------+---------------+----------------|
| src/LuLuCoin.sol | 50.00% (3/6) | 50.00% (2/4) | 100.00% (0/0) | 50.00% (1/2) |
|-------------------+----------------+----------------+---------------+----------------|
| Total | 89.74% (35/39) | 90.32% (28/31) | 75.00% (3/4) | 90.91% (10/11) |
╰-------------------+----------------+----------------+---------------+----------------╯
部署合约
准备把合约部署在本地网,新建.env文件
OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
OWNER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
USER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
USER_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
MINT_AMOUNT=1000000000000000000000
DEPOSIT_AMOUNT=800000000000000000000
DRIP_AMOUNT=800000000000000000000
DRIP_LIMIT=800000000000000000000
DRIP_INTERVAL=10
LLC_CONTRACT=0x5FbDB2315678afecb367f032d93F642f64180aa3
FAUCET_CONTRACT=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
.env文件里的所有参数,key与value之间不能有任何空格
# .env 配置文件包含所有敏感的环境变量,确保安全性和可配置性
include .env
# 引入 .env 文件,确保环境变量能够在 Makefile 中使用
deploy_lulucoin:
@echo "Deploying LuLuCoin contract..."
# 输出部署信息,提示正在部署 LuLuCoin 合约
forge create src/LuLuCoin.sol:LuLuCoin --private-key ${OWNER_PRIVATE_KEY} --broadcast --constructor-args ${OWNER_ADDRESS}
# 使用 forge 部署 LuLuCoin 合约,传入合约所有者的私钥和地址作为构造函数参数
deploy_faucet:
@echo "Deploying LLCFaucet contract..."
# 输出部署信息,提示正在部署 LLCFaucet 合约
forge create --private-key ${OWNER_PRIVATE_KEY} src/LLCFaucet.sol:LLCFaucet --broadcast --constructor-args ${LLC_CONTRACT} ${DRIP_INTERVAL} ${DRIP_LIMIT} ${OWNER_ADDRESS}
# 使用 forge 部署 LLCFaucet 合约,传入相关的合约地址、领取间隔、领取限制和合约所有者的地址作为构造函数参数
mint:
@echo "Minting tokens..."
# 输出铸币信息,提示正在铸造代币
cast send ${LLC_CONTRACT} "mint(uint256)" ${MINT_AMOUNT} --private-key ${OWNER_PRIVATE_KEY}
# 使用 cast 发送交易调用 mint 函数,铸造指定数量的代币
balance_of_owner:
@echo "Getting balance of the owner..."
# 输出查询余额信息,提示正在查询合约所有者的余额
cast call ${LLC_CONTRACT} "balanceOf(address)" ${OWNER_ADDRESS}
# 使用 cast 调用 `balanceOf` 函数,查询合约所有者的余额
approve_faucet:
@echo "Approving faucet contract to spend tokens..."
# 输出批准信息,提示正在批准水龙头合约能够支出代币
cast send ${LLC_CONTRACT} "approve(address,uint256)" ${FAUCET_CONTRACT} ${DEPOSIT_AMOUNT} --private-key ${OWNER_PRIVATE_KEY}
# 使用 cast 发送交易调用 `approve` 函数,允许水龙头合约支出指定数量的代币
drip:
@echo "Dripping tokens from faucet..."
# 输出领取信息,提示正在从水龙头领取代币
cast send ${FAUCET_CONTRACT} "drip(uint256)" ${DRIP_AMOUNT} --private-key ${USER_PRIVATE_KEY}
# 使用 cast 发送交易调用 `drip` 函数,用户从水龙头领取指定数量的代币
deposit:
@echo "Depositing tokens to faucet..."
# 输出存款信息,提示正在向水龙头存入代币
cast send ${FAUCET_CONTRACT} "deposit(uint256)" ${DEPOSIT_AMOUNT} --private-key ${OWNER_PRIVATE_KEY}
# 使用 cast 发送交易调用 `deposit` 函数,合约所有者向水龙头存入指定数量的代币
balance_of_faucet:
@echo "Getting balance of the faucet contract..."
# 输出查询余额信息,提示正在查询水龙头合约的代币余额
cast call ${LLC_CONTRACT} "balanceOf(address)" ${FAUCET_CONTRACT}
# 使用 cast 调用 `balanceOf` 函数,查询水龙头合约的余额
balance_of_user:
@echo "Getting balance of the faucet contract..."
# 输出查询余额信息,提示正在查询用户的代币余额
cast call ${LLC_CONTRACT} "balanceOf(address)" ${USER_ADDRESS}
# 使用 cast 调用 `balanceOf` 函数,查询用户的余额
make和.env文件必须位于contracts目录下,否则找不到文件
环境变量生效,使用makefile部署合约以及交互
部署LuLuCoin合约
$ source .env
$ make deploy_lulucoin
Deploying LuLuCoin contract...
[⠒] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xde5436f283429964c4e5cad6e2cdfa1c15fa8a810ba1d0d84c854d2c69937698
已经部署的LuLuCoin合约地址Deployed to
部署LLCFaucet合约
$ make deploy_faucet
Deploying LLCFaucet contract...
[⠒] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Transaction hash: 0x0524a858595c8727f93b92f75fd694326d3c3e146188e34ae3b072f86353c0b4
已经部署的LLCFaucet合约地址Deployed to
把这两个合约地址粘贴回到.env文件中,并保存
铸造代币
$ make mint
Minting tokens...
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 71831
status 1 (success)
transactionHash 0xdff2c8b6707b4d1091200fb2f47f820d5c6c3c5617ec010346540670b97891c0
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
检查owner余额,为1000个
$ make balance_of_owner
Getting balance of the owner...
0x00000000000000000000000000000000000000000000003635c9adc5dea00000
zhangyu@zhangyu:~/Web3Tutorial/02faucet/contracts
$ cast to-dec 0x00000000000000000000000000000000000000000000003635c9adc5dea00000
1000000000000000000000
LuLuCoin合约+LLCFaucet合约是同一个所有者OWNER_ADDRESS
owner授权LuLuCoin合约可以支出代币
$ make approve_faucet
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 46963
status 1 (success)
transactionHash 0xf9abb738c01e5d86bd368b16e836fa408439f2c7490dd6b247da95eeb1b718da
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
owner向LLCFaucet合约存款
$ make deposit
Depositing tokens to faucet...
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 64130
status 1 (success)
transactionHash 0xfcbf282d22ebccdf342c4dba59cdbc29d438a2d12aeee837688a6bf89df8435e
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
检查owner余额,为200个
$ make balance_of_owner
Getting balance of the owner...
0x00000000000000000000000000000000000000000000000ad78ebc5ac6200000
$ cast to-dec 0x00000000000000000000000000000000000000000000000ad78ebc5ac6200000
200000000000000000000
检查水龙头余额,为800个
$ make balance_of_faucet
Getting balance of the faucet contract...
0x00000000000000000000000000000000000000000000002b5e3af16b18800000
$ cast to-dec 0x00000000000000000000000000000000000000000000002b5e3af16b18800000
800000000000000000000
owner铸造1000个,向LLCFaucet合约存款800个,现在自身剩余200个,正确
user从水龙头领取代币
$ make drip
Dripping tokens from faucet...
from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
gasUsed 87841
status 1 (success)
transactionHash 0x4859f33db229294cb4436984a4b9b99955d03d19f70d7404ff7808bb5690151a
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
查看user代币余额,为1个
$ make balance_of_user
Getting balance of the faucet contract...0x0000000000000000000000000000000000000000000000000de0b6b3a7640000
$ cast to-dec 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000
1000000000000000000
查看此时水龙头余额,为799个
$ make balance_of_faucet
Getting balance of the faucet contract...
0x00000000000000000000000000000000000000000000002b505a3ab7711c0000
$ cast to-dec 0x00000000000000000000000000000000000000000000002b505a3ab7711c0000
799000000000000000000
连续两次调用drip
$make drip
Dripping tokens from faucet...
from 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
gasUsed 53641
status 1 (success)
transactionHash 0x51932294eff13128d423675c89ea7ba6b26d6da98b7cc85e855d0a621bae524d
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
$ make drip
Dripping tokens from faucet...
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xe28e4f7b, data: "0xe28e4f7b": LLCFaucet__IntervalHasNotPassed
make: *** [makefile:38:drip] 错误 1
第一次成功,第二次失败,因为间隔小于10秒钟
查看该错误代码
$ forge selectors find 0xe28e4f7b
Searching for selector "0xe28e4f7b" in the project...
Found 1 instance(s)...
╭-------+-----------------------------------+------------+-----------╮
| Type | Signature | Selector | Contract |
+====================================================================+
| Error | LLCFaucet__IntervalHasNotPassed() | 0xe28e4f7b | LLCFaucet |
╰-------+-----------------------------------+------------+-----------╯
回显,水龙头领取间隔未通过,符合测试逻辑
如果把用户单次领取金额改为800个,即DRIP_AMOUNT=800000000000000000000,再运行drip报错。因为用户领取的上限DRIP_LIMIT没改还是之前的100个
$ make drip
Dripping tokens from faucet...
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0xa27a5db0, data: "0xa27a5db0": LLCFaucet__ExceedLimit
make: *** [makefile:38:drip] 错误 1
$ forge selectors find 0xa27a5db0
Searching for selector "0xa27a5db0" in the project...
Found 1 instance(s)...
╭-------+--------------------------+------------+-----------╮
| Type | Signature | Selector | Contract |
+===========================================================+
| Error | LLCFaucet__ExceedLimit() | 0xa27a5db0 | LLCFaucet |
╰-------+--------------------------+------------+-----------╯
同时用owner把领取金额上限改为800个,即DRIP_LIMIT=800000000000000000000,即此时单次领取DRIP_LIMIT和领取上限DRIP_LIMIT都是800个
$ cast send ${FAUCET_CONTRACT} "setDripLimit(uint256)" ${DRIP_LIMIT} --private-key ${OWNER_PRIVATE_KEY}
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 28974
status 1 (success)
transactionHash 0xd967cb9e5bb7cc1be3fa20fe0bc3b26c1eb2ac0d570837e78eac69dbc884202f
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
目前水龙头合约余额798个,用户一次性领取800个,即DRIP_AMOUNT=800000000000000000000
$ make drip
Dripping tokens from faucet...
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x8005eae6, data: "0x8005eae6": LLCFaucet__FaucetEmpty
make: *** [makefile:38:drip] 错误 1
$ forge selectors find 0x8005eae6
Searching for selector "0x8005eae6" in the project...
Found 1 instance(s)...
╭-------+--------------------------+------------+-----------╮
| Type | Signature | Selector | Contract |
+===========================================================+
| Error | LLCFaucet__FaucetEmpty() | 0x8005eae6 | LLCFaucet |
╰-------+--------------------------+------------+-----------╯
回显报错,因为水龙头当前共798个,用户提取800个,超出所以回滚