Skip to main content

Command Palette

Search for a command to run...

使用foundry编写、测试和部署水龙头合约

Published
10 min readView as Markdown
C
酷愛計算機技術Ardently Love Computer Technology 長期關註反洗錢反欺詐Long-term Focus on Anti-Money Laundering and Anti-Fraud 精通支付結算的技術、系統、流程和製度Proficient in the Technology, System, Process and Institution of Payment and Settlement
  • 安装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个,超出所以回滚

More from this blog

Penetration Test、Python、Weaponization

721 posts

微信 smartcat9999 反欺詐Anti-Fraud 反洗錢Anti-Money Laundering 反逃稅Anti-Tax Evasion 滲透測試Penetration Test 武器化Weaponization