Ethereum 开发工具之 Foundry

Ethereum 开发工具之 Foundry

一. Foundy 简介

Foundry是一套专为以太坊智能合约开发而设计的工具链,旨在提升开发者的工作效率和开发体验。Foundry提供了一系列功能强大的工具,支持快速的合约编写、测试、部署和调试。以下是Foundry合约开发工具的主要组成部分和功能介绍:

1.主要组成部分

1.1.Forge

  • Forge是Foundry的核心工具,用于编译、测试和部署智能合约。
  • 它支持Solidity和Yul语言,提供了高效的编译器,并与EVM(以太坊虚拟机)紧密集成。
  • Forge允许开发者轻松运行单元测试、覆盖率测试和模拟测试环境。

1.2.Cast

  • Cast是Foundry的命令行工具,用于与以太坊区块链进行交互。
  • 它支持常见的区块链操作,例如查询区块、交易、账户余额和发送交易。
  • Cast还支持脚本编写,帮助开发者自动化重复的区块链交互操作。

1.3.Anvil

  • Anvil是Foundry的本地开发节点,用于模拟以太坊网络环境。
  • 它提供了一个轻量级的本地测试网络,支持快速的合约部署和测试。
  • Anvil允许开发者在本地调试合约,方便快速迭代和调整。

2.功能介绍

2.1.高效的编译器

  • Foundry的编译器优化了合约的编译速度和生成的字节码质量。
  • 支持Solidity和Yul,兼容主流的Solidity版本。

2.2.强大的测试框架

  • Forge提供了全面的测试功能,包括单元测试、集成测试和覆盖率测试。
  • 测试框架支持断言和模拟,帮助开发者验证合约逻辑的正确性。

2.3.调试和分析工具

  • Foundry包含强大的调试功能,支持断点、变量监视和交易回溯。
  • 开发者可以深入分析合约执行过程,找出潜在的问题和性能瓶颈。

2.4.便捷的部署和管理

  • Forge支持一键部署合约到本地或远程的以太坊网络。
  • 开发者可以轻松管理多个部署环境和合约版本。

2.5.脚本和自动化

  • Cast工具支持编写脚本,自动化常见的区块链交互任务。
  • 支持复杂的脚本逻辑,帮助开发者提高工作效率。

二.安装与简单命令

安装:Foundry可以通过命令行工具安装,首先需要安装Rust,然后通过Cargo 安装 Foundry:

curl -L https://foundry.paradigm.xyz | bash
foundryup

初始化项目:创建新的Foundry项目:

forge init your-project

编译合约:编译 Solidity 合约:

forge build

运行测试:运行测试用例:

forge test

部署合约

forge script script/xxx.s.sol:xxxScript --rpc-url <your_rpc_url> --private-key <your_private_key>

验证合约:步骤分为生成验证数据和执行验证命令

  • 设置 EtherScan API Key
export ETHERSCAN_API_KEY=G2KQYXZEAV4H3GB1A1T6N832412D4648KH
  • 执行验证命令
forge verify-contract --chain-id 11155111 0x599a22f498e5e0000440860ee4ff1dd8a5f06b66 src/contracts/OneDollarTreasure.sol:OneDollarTreasure

与区块链交互:使用Cast与区块链交互:

cast send --from <address> <contract-address> <method> <args>

三. 使用 Foundry 工具开发上线一元夺宝游戏

1.初始化项目

  • 执行初始化项目命令
forge init one-dollar-treasure
  • 生成代码结构如下

  • lib目录:lib 目录下放置的是各种第三方的依赖库,例如 OZ 代码库
  • script 目录:放置部署脚本的目录
  • Src 目录:放置合约项目核心代码的目录
  • Test 目录:放置测试脚本的目录
  • foundry.toml:foundry 工程配置文件

2.代码编写

我们得到以上项目之后,需要将一元夺宝的项目开发出来,一元夺宝的逻辑是,每个人都投入 1USDT 进入合约,周期性开奖,达到周期之后开奖,选择一个幸运用户拿资金池的 98% 的 USDT,剩下的 2% 转入到项目方的国库合约做为平台运营手续费。

  • 投注函数:用户投注函数
function betting(address better, uint256 amount) external {
    if (IERC20(tokenAddress).balanceOf(better) < amount) {
        revert NotEnoughToken(address(tokenAddress));
    }
    if (amount < 10 ** 6) {
        revert NotRightAmountToken(address(tokenAddress));
    }

    tokenAddress.safeTransferFrom(better, address(this), amount);
    if (roundBetting[roundNumber].totalAmount <= 0) {
        RoundBettingInfo memory rBInfo = RoundBettingInfo({
            totalAmount: amount,
            status:0
        });
        roundBetting[roundNumber] = rBInfo;
    } else {
        roundBetting[roundNumber].totalAmount += amount;
    }
    bettingMembers[roundNumber].push(better);
    emit BettingInfo(
        better,
        amount
    );
}
  • 开奖函数,到达周期之后开奖并开启下一期
function lotteryAndGenerateNewRound(uint256 roundSeed) external {
    if (block.timestamp < roundEndTime[roundNumber]) {
        revert ThisPeriodNoExpiration();
    }
    address[] memory addressList = bettingMembers[roundNumber];

    address rewardAddress = addressList[roundSeed];
    uint256 rewardAmount = (roundBetting[roundNumber].totalAmount * 95) / 100;
    uint256 middleFee =  roundBetting[roundNumber].totalAmount - rewardAmount;

    if (roundSeed > addressList.length) {
        revert RoundNumberIsBig();
    }

    tokenAddress.safeTransferFrom(address(this), rewardAddress, rewardAmount);

    tokenAddress.safeTransferFrom(address(this), TheWeb3TreasureAddress, middleFee);

    emit LotteryAndGenerateNewRound(
        rewardAddress,
        rewardAmount,
        middleFee,
        roundNumber
    );

    roundNumber++;

    roundEndTime[roundNumber] = block.timestamp + periodTime;
}
  • 完成代码如下:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import "./OneDollarTreasureStorage.sol";
import "forge-std/console.sol";

contract OneDollarTreasure is  Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, OneDollarTreasureStorage {
    address public constant TheWeb3TreasureAddress = address(0xe3b4ECd2EC88026F84cF17fef8bABfD9184C94F0);

    using SafeERC20 for IERC20;

    uint32 public periodTime;

    IERC20 public tokenAddress;

    function initialize(IERC20 _tokenAddress) public initializer {
        roundNumber = 1;
        roundAmount = 0;
        periodTime = 21 days;
        roundEndTime[roundNumber] = block.timestamp + periodTime;
        tokenAddress = _tokenAddress;
    }

    function betting(address better, uint256 amount) external {
        if (IERC20(tokenAddress).balanceOf(better) < amount) {
            revert NotEnoughToken(address(tokenAddress));
        }
        if (amount < 10 ** 6) {
            revert NotRightAmountToken(address(tokenAddress));
        }

        tokenAddress.safeTransferFrom(better, address(this), amount);
        if (roundBetting[roundNumber].totalAmount <= 0) {
            RoundBettingInfo memory rBInfo = RoundBettingInfo({
                totalAmount: amount,
                status:0
            });
            roundBetting[roundNumber] = rBInfo;
        } else {
            roundBetting[roundNumber].totalAmount += amount;
        }
        bettingMembers[roundNumber].push(better);
        emit BettingInfo(
            better,
            amount
        );
    }

    function lotteryAndGenerateNewRound(uint256 roundSeed) external {
        if (block.timestamp < roundEndTime[roundNumber]) {
            revert ThisPeriodNoExpiration();
        }
        address[] memory addressList = bettingMembers[roundNumber];

        address rewardAddress = addressList[roundSeed];
        uint256 rewardAmount = (roundBetting[roundNumber].totalAmount * 95) / 100;
        uint256 middleFee =  roundBetting[roundNumber].totalAmount - rewardAmount;

        if (roundSeed > addressList.length) {
            revert RoundNumberIsBig();
        }

        tokenAddress.safeTransferFrom(address(this), rewardAddress, rewardAmount);

        tokenAddress.safeTransferFrom(address(this), TheWeb3TreasureAddress, middleFee);

        emit LotteryAndGenerateNewRound(
            rewardAddress,
            rewardAmount,
            middleFee,
            roundNumber
        );

        roundNumber++;

        roundEndTime[roundNumber] = block.timestamp + periodTime;
    }

    function getBettingRound(uint256 roundNumber) external returns (RoundBettingInfo memory) {
        return roundBetting[roundNumber];
    }
}

3.测试

  • 编写 testBetting 测试脚本,由于该项目是做示范讲解,因此我们不编写全量的测试代码,只做简单示范
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";


import "forge-std/Test.sol";
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/contracts/OneDollarTreasure.sol";
import "../src/access/proxy/Proxy.sol";

import "./DappLinkToken.sol";


contract OneDollarTreasureTest is Script, Test {
    using SafeERC20 for IERC20;

    OneDollarTreasure  public oneDollarTreasure;
    DappLinkToken public dappLinkToken;

    Proxy public proxyOneDollarTreasure;
    Proxy public proxyDappLinkToken;

    function setUp() external {
        vm.startBroadcast();
        address admin = msg.sender;

        dappLinkToken = new DappLinkToken();
        proxyDappLinkToken = new Proxy(address(dappLinkToken), address(admin), "");
        DappLinkToken(address(proxyDappLinkToken)).initialize(admin);

        oneDollarTreasure = new OneDollarTreasure();
        proxyOneDollarTreasure = new Proxy(address(oneDollarTreasure), address(admin), "");
        OneDollarTreasure(address(proxyOneDollarTreasure)).initialize(IERC20(address(proxyDappLinkToken)));

        vm.stopBroadcast();
    }

    function testBetting() public {
        uint256 amount = 10 ** 6;
        vm.prank(msg.sender);
        DappLinkToken(address(proxyDappLinkToken)).approve(address(proxyOneDollarTreasure), amount * 10);

        vm.prank(msg.sender);
        OneDollarTreasure(address(proxyOneDollarTreasure)).betting(msg.sender, amount);

        OneDollarTreasure.RoundBettingInfo memory rBinfoItme = OneDollarTreasure(address(proxyOneDollarTreasure)).getBettingRound(1);

        assertEq(rBinfoItme.totalAmount, 1000000);
        assertEq(rBinfoItme.status, 0);
    }
}
  • 执行测试
forge test -vv
  • 输出结果
[⠊] Compiling...
[⠒] Compiling 2 files with Solc 0.8.24
[⠢] Solc 0.8.24 finished in 1.03s
Compiler run successful!

Ran 1 test for test/OneDollarTreasure.sol:OneDollarTreasureTest
[PASS] testBetting() (gas: 160879)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 767.08µs (94.92µs CPU time)

Ran 1 test suite in 112.23ms (767.08µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

4.部署

编写部署脚本

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

import {Script, console} from "forge-std/Script.sol";
import "../src/contracts/OneDollarTreasure.sol";
import "../src/access/proxy/Proxy.sol";

contract OneDollarTreasureScript is Script {
    OneDollarTreasure  public oneDollarTreasure;

    Proxy public proxyOneDollarTreasure;

    function run() public {
        vm.startBroadcast();
        address admin = msg.sender;

        IERC20 tokenAddress = IERC20(address(0xe3b4ECd2EC88026F84cF17fef8bABfD9184C94F0));

        oneDollarTreasure = new OneDollarTreasure();
        proxyOneDollarTreasure = new Proxy(address(oneDollarTreasure), address(admin), "");
        OneDollarTreasure(address(proxyOneDollarTreasure)).initialize(tokenAddress);

        vm.stopBroadcast();
    }
}

将代码部署到 sepolia 网络

  • 部署命令
forge script script/OneDollarTreasure.s.sol:OneDollarTreasureScript --rpc-url $RPC_URL --private-key $PRIVKEY
  • 返回值
[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.24
[⠃] Solc 0.8.24 finished in 748.28ms
Compiler run successful!
Script ran successfully.

## Setting up 1 EVM.

==========================

Chain 11155111

Estimated gas price: 52.628255844 gwei

Estimated total gas used for script: 2232183

Estimated amount required: 0.117475898014627452 ETH

==========================

SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.

Transactions saved to: /Users/guoshijiang/theweb3/one-dollar-treasure/broadcast/OneDollarTreasure.s.sol/11155111/dry-run/run-latest.json

Sensitive values saved to: /Users/guoshijiang/theweb3/one-dollar-treasure/cache/OneDollarTreasure.s.sol/11155111/dry-run/run-latest.json

在 sepolia 网络上验证合约代码

  • 命令行
export ETHERSCAN_API_KEY=TNSTBJQHXQV8FJDC8BXEYCQXJP39TJ3U7U
guoshijiang@192 one-dollar-treasure % forge verify-contract --chain-id 11155111 0x599a22f498e5e0000440860ee4ff1dd8a5f06b66 src/contracts/OneDollarTreasure.sol:OneDollarTreasure
  • 返回值

5.项目完整截图

我们可以看到,这里和上面的代码相比多了一个 remappings.txt 文件,remappings.txt 文件用于定义路径重映射,以简化和管理项目中的导入路径。这对于大型项目或依赖于外部库的项目尤其有用,因为它允许开发者通过短路径或别名引用库和模块,而不是使用长且复杂的相对路径

  • 本项目的 remappings.txt 代码如下:
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
ds-test/=lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/
openzeppelin-contracts/=lib/openzeppelin-contracts/

remappings.txt 文件具有一下几个作用

  • 路径别名: remappings.txt 可以将复杂的路径映射到更简单的别名,使得代码中的导入路径更简洁易读。
  • 依赖管理: 对于使用外部库的项目,remappings.txt 可以统一管理这些库的路径,避免在代码中硬编码路径。
  • 提升可读性: 简化后的路径使得代码更易于阅读和维护,特别是在大型项目中,路径重映射可以显著提升代码的整洁度。

6.cast 交互测试

  • Cast 命令投注
cast send --rpc-url $RPC_URL --private-key $PRIVATE_KEY $CONTRACT_ADDRESS " betting(address,uint256)" 0xe3b4ECd2EC88026F84cF17fef8bABfD9184C94F0 10000000
  • Cast 查看投注信息
cast call --rpc-url $RPC_URL "getBettingRound(uint256)" 1

全部评论(0)