Skip to content

在Moonbeam上使用Foundry

作者:Jeremy Boetticher

概览

Foundry已成为越来越受欢迎的用于开发智能合约的开发环境,因其只需要一种语言(Solidity)即可使用。建议您在开始使用Foundry之前,先阅读在Moonbeam网络上使用Foundry的介绍文章。在本教程中,我们将深入代码库,以全面了解如何正确开发、测试和部署。

在本次操作演示中,我们将部署两个智能合约。一个是Token,另一个将基于此Token上。我们也将编写单元测试以确保合约如预期运作。要部署合约,我们要先编写脚本,Foundry将使用此脚本来决定部署逻辑。最后,我们要在Moonbeam网络的区块浏览器上验证智能合约。

查看先决条件

开始之前,您将需要准备以下内容:

创建Foundry项目

首先,创建一个Foundry项目。如果您已安装Foundry,您可以运行以下命令:

forge init foundry && cd foundry

这将使forge实用程序初始化一个名为foundry的新文件夹,并在其中初始化一个Foundry项目。scriptsrctest文件夹中可能已经有文件。请确保将其删除,因为我们会在之后编写自己的文件。

在编写任何代码之前,您需要先做一些事情。首先,我们要添加对OpenZeppelin的智能合约的依赖,因其包含一些有用的合约,将用于编写Token智能合约。为此,使用其GitHub代码库名称来完成添加:

forge install OpenZeppelin/openzeppelin-contracts

这会将OpenZeppelin git子模块添加到您的lib文件夹中。 为确保此依赖项已映射,您可以覆盖特殊文件remappings.txt中的映射:

forge remappings > remappings.txt

该文件中的每一行都是可以在项目的智能合约中引用的依赖项之一。可以编辑和重命名依赖项,以便在处理智能合约时更容易引用不同的文件夹和文件。如果正确安装了OpenZeppelin,将显示与下方类似的输出:

ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/

最后,打开foundry.toml文件。在准备Etherscan验证和部署时,将此添加到文件中:

[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
solc_version = '0.8.17'

[rpc_endpoints]
moonbase = "https://rpc.api.moonbase.moonbeam.network"
moonbeam = "https://rpc.api.moobeam.network"

[etherscan]
moonbase = { key = "${MOONSCAN_API_KEY}" }
moonbeam = { key = "${MOONSCAN_API_KEY}" }

第一个添加的是profile.default下面的solc_version的规范。rpc_endpoints标签允许您定义在部署到命名网络时要使用的RPC端点,在本例中为Moonbase Alpha和Moonbeam。etherscan标签允许您添加用于智能合约验证的Etherscan API密钥,我们将在之后展开讨论。

添加智能合约

Foundry中默认部署的智能合约属于src文件夹。 在本教程中,我们将编写两个智能合约。 首先从Token开始:

touch MyToken.sol

打开文件并添加以下内容:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Import OpenZeppelin Contract
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

// This ERC-20 contract mints the specified amount of tokens to the contract creator
contract MyToken is ERC20 {
  constructor(uint256 initialSupply) ERC20("MyToken", "MYTOK") {
    _mint(msg.sender, initialSupply);
  }

  // An external minting function allows anyone to mint as many tokens as they want
  function mint(uint256 toMint, address to) external {
    require(toMint <= 1 ether);
    _mint(to, toMint);
  }
}

如您所见,OpenZeppelin ERC20智能合约是通过remappings.txt中定义的映射导入的。

第二个智能合约(我们将其命名为Container.sol)将依赖于这个Token合约。这是一个简单的合约,包含我们将要部署的ERC-20 Token。您可以通过执行以下命令来创建文件:

touch Container.sol

打开文件并添加以下内容:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Import OpenZeppelin Contract
import {MyToken} from "./MyToken.sol";

enum ContainerStatus {
    Unsatisfied,
    Full,
    Overflowing
}

contract Container {
    MyToken token;
    uint256 capacity;
    ContainerStatus public status;

    constructor(MyToken _token, uint256 _capacity) {
        token = _token;
        capacity = _capacity;
        status = ContainerStatus.Unsatisfied;
    }

    // Updates the status value based on the number of tokens that this contract has
    function updateStatus() public {
        address container = address(this);
        uint256 balance = token.balanceOf(container);
        if (balance < capacity) {
            status = ContainerStatus.Unsatisfied;
        } else if (balance == capacity) {
            status = ContainerStatus.Full;
        } else if (_isOverflowing(balance)) {
            status = ContainerStatus.Overflowing;
        }
    }

    // Returns true if the contract should be in an overflowing state, false if otherwise
    function _isOverflowing(uint256 balance) internal view returns (bool) {
        return balance > capacity;
    }
}

Container智能合约可以根据其持有的Token数量及其设置的初始容量值来更新其状态。如果只有的Token数量大于其容量,则状态可以更新为Overflowing。如果持有的Token数量等于容量,则状态可以更新为Full。否则,合约将开始并保持Unsatisfied状态。

Container需要一个MyToken智能合约实例才能运行,因此当我们部署时,我们需要逻辑来确保其与MyToken智能合约实例一起部署。

编写测试

在我们部署任何合约至测试网或主网前,建议您先测试您的智能合约。多种测试类型如下所示:

  • 单元测试(Unit tests) — 允许您测试智能合约功能的特定部分。当您编写自己的智能合约时,建议您将功能分成不同的部分,以便进行单元测试
  • 模糊测试(Fuzz tests) — 允许您使用各种输入测试智能合约以检查边缘情况
  • 集成测试(Integration tests) — 允许您在智能合约与其他智能合约一起工作时对其进行测试,以便您了解其能够在已部署的环境中按预期运作
    • 分叉测试(Forking tests) - 允许您创建分叉(网络的副本)的集成测试,以便您可以在预先存在的网络上模拟一系列交易

Foundry中的单元测试

要开始为此教程编写测试,先在test文件中创建一个新的文件:

cd test
touch MyToken.t.sol

根据惯例,所有的测试需要以.t.sol结尾并以正在测试的智能合约名称开始。实际上,测试可以存储在任何地方,如果有以"test"开始的函数,则被视为测试。

接下来,开始为Token智能合约编写测试。打开MyToken.t.sol并添加以下内容:

pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/MyToken.sol";

contract MyTokenTest is Test {
    MyToken public token;

    // Runs before each test
    function setUp() public {
        token = new MyToken(100);
    }

    // Tests if minting during the constructor happens properly
    function testConstructorMint() public {
        assertEq(token.balanceOf(address(this)), 100);
    }
}

我们来分析一下此处的代码:第一行是典型的Solidity文件:设置Solidity版本。接下来的两行是导入。forge-std/Test.sol是Forge(也就是Foundry)包含的用于帮助测试的标准库。这包括Test智能合约,某些断言(assertion)和forge cheatcodes

如果您查看MyTokenTest智能合约,您将看到两个函数。第一个是setUp,在每个测试之前运行。因此在此测试合约中,每次运行测试函数时都会部署MyToken新实例。如果函数以"test" 开头,则该函数为测试函数。所以第二个函数testConstructorMint是一个测试函数。

现在,我们再写一些关于Container的测试:

touch Container.t.sol

然后添加以下内容:

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

import "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
import {Container, ContainerStatus} from "../src/Container.sol";

contract ContainerTest is Test {
    MyToken public token;
    Container public container;

    uint256 constant CAPACITY = 100;

    // Runs before each test
    function setUp() public {
        token = new MyToken(1000);
        container = new Container(token, CAPACITY);
    }

    // Tests if the container is unsatisfied right after constructing
    function testInitialUnsatisfied() public {
        assertEq(token.balanceOf(address(container)), 0);
        assertTrue(container.status() == ContainerStatus.Unsatisfied);
    }

    // Tests if the container will be "full" once it reaches its capacity
    function testContainerFull() public {
        token.transfer(address(container), CAPACITY);
        container.updateStatus();

        assertEq(token.balanceOf(address(container)), CAPACITY);
        assertTrue(container.status() == ContainerStatus.Full);
    }
}

此测试合约有两个测试,所以在运行测试时,会有MyTokenContainer的两次部署,总共为4个智能合约。您可以运行以下命令来查看测试结果:

forge test

测试时,您将看到以下输出:

Unit Testing in Foundry

Foundry中的测试套件(Test Harness)

有时候您会希望在智能合约中对internal函数进行单元测试。为此,您需要编写测试套件智能合约,其继承自智能合约并将内部函数公开作为公共函数。

举例来说,在Container中有一个名为_isOverflowing的内部函数,用于检查智能合约是否有比容量更多的Token。要测试此函数,请添加以下测试套件智能合约至Container.t.sol文件中:

contract ContainerHarness is Container {
    constructor(MyToken _token, uint256 _capacity) Container(_token, _capacity) {}

    function exposed_isOverflowing(uint256 balance) external view returns(bool) {
        return _isOverflowing(balance);
    }
}

ContainerTest智能合约里面,您可以添加一个新测试,用于测试之前不可读取的_isOverflowing合约:

    // Tests for negative cases of the internal _isOverflowing function
    function testIsOverflowingFalse() public {
        ContainerHarness harness = new ContainerHarness(token , CAPACITY);
        assertFalse(harness.exposed_isOverflowing(CAPACITY - 1));
        assertFalse(harness.exposed_isOverflowing(CAPACITY));
        assertFalse(harness.exposed_isOverflowing(0));
    }

现在,当您用forge test运行测试时,您将看到testIsOverflowingFalse已经通过!

Test Harness in Foundry

Foundry中的模糊测试

当您编写单元测试时,您只能使用一些输入进行测试。您可以尝试测试边缘情况,选择一些值,可以是一个或者两个随机值。但是在处理输入时,有无数种不同的输入需要测试。如何保证其适用于每个值?如果把少于10个的输入替换成10000个不同的输入,会不会更安全呢?

开发者可以测试很多输入的最佳方式之一是通过模糊测试(fuzzing或fuzz测试)。当测试函数中包含输入时,Foundry会自动进行模糊测试。为了说明这一点,将以下测试添加到MyToken.t.sol中的MyTokenTest合约。

    // Fuzz tests for success upon minting tokens one ether or below
    function testMintOneEtherOrBelow(uint256 amountToMint) public {
        vm.assume(amountToMint <= 1 ether);

        token.mint(amountToMint, msg.sender);
        assertEq(token.balanceOf(msg.sender), amountToMint);
    }

这些测试包含uint256 amountToMint作为输入,告知Foundry使用uint256输入进行模糊测试。默认情况下,Foundry将输入256个不同的输入,但可以使用FOUNDRY_FUZZ_RUNS环境变量配置。

此外,该函数的第一行使用vm.assume以仅使用小于等于1 ether的输入,如果有人试图一次铸造超过1 ether,则mint函数将返还。此cheatcode可以帮助您将模糊测试引导到正确的范围。

我们来看一下另一个放入MyTokenTest合约中的模糊测试,但是我们预计会失败:

    // Fuzz tests for failure upon minting tokens above one ether
    function testFailMintAboveOneEther(uint256 amountToMint) public {
        vm.assume(amountToMint > 1 ether);

        token.mint(amountToMint, msg.sender);
    }

在Foundry中,当你预想到测试会失败,您无需以"test"开始,可以直接以"testFail"开始测试函数名。在此测试中,我们假设amountToMint超过1 ether(即结果会失败)。

现在运行测试:

forge test

您将在控制台看到以下类似输出:

Fuzzing Tests in Foundry

Foundry中的分叉测试

在Foundry中,您可以在本地分叉网络,从而您可以测试合约如何在已部署合约的环境中运行。举例来说,如果有人在Moonbeam上已部署需要Token智能合约的智能合约A,您可以分叉Moonbeam网络并在分叉上部署您自己的Token以测试智能合约A如何反应。

注意事项

Moonbeam的自定义预编译合约目前暂不可在Foundry分叉中使用,因为预编译是基于Substrate,而典型的合约是完全基于EVM。更多内容请参考:Moonbeam上分叉Moonbeam和以太坊之间的异同之处.

在本教程中,您将测试Container智能合约如何在Moonbase Alpha上与已部署的MyToken合约交互。

将名为testAlternateTokenOnMoonbaseFork的新测试函数添加至Container.t.sol中的ContainerTest智能合约。

    // Fork tests in the Moonbase Alpha environment
    function testAlternateTokenOnMoonbaseFork() public {
        // Creates and selects a fork, returns a fork ID
        uint256 moonbaseFork = vm.createFork("moonbase");
        vm.selectFork(moonbaseFork);
        assertEq(vm.activeFork(), moonbaseFork);

        // Get token that's already deployed & deploys a container instance
        token = MyToken(0x359436610E917e477D73d8946C2A2505765ACe90);
        container = new Container(token, CAPACITY);

        // Mint tokens to the container & update container status
        token.mint(CAPACITY, address(container));
        container.updateStatus();

        // Assert that the capacity is full, just like the rest of the time
        assertEq(token.balanceOf(address(container)), CAPACITY);
        assertTrue(container.status() == ContainerStatus.Full);
    }

此函数的第一步(也是第一行)是让测试函数使用vm.createFork分叉网络。vm是由Forge标准库提供的cheatcode。创建分叉需要的是一个RPC URL或者存储在foundry.toml文件中的RPC URL别名。在本示例中,我们在设置步骤中为"moonbase"添加了一个RPC URL,因此在测试函数中我们将只需传递"moonbase"。此cheatcode函数返回创建分叉的ID,该ID存储在uint256中,是激活分叉所必需的。

在第二行中,即分叉创建后,将通过vm.selectFork在测试环境中选择和使用分叉。第三行只是为了证明当前的分叉,通过vm.activeFork检索,与Moonbase Alpha分叉相同。

第四行代码检索已部署的MyToken实例,这就是分叉的用武之地:您可以使用已经部署的合约。

剩下的代码测试容量,就像您期望的本地测试一样。如果您运行测试(使用-vvvv标签可以得到额外的日志),您将看到测试通过了:

forge test -vvvv

Forking Tests in Foundry

这就是测试的步骤!您可以在GitHub上查看完整的Container.t.sol文件MyToken.t.sol文件

使用Solidity脚本在Foundry中部署

Foundry中的测试和脚本均以Solidity编写。与其他开发者环境一样,可以编写脚本来帮助与已部署智能合约交互,或协助完成手动难以实现的复杂部署流程。即使脚本是用Solidity编写,但是这些脚本不会部署到链上。相反,大部分的逻辑实际上是在链下运行的,因此使用Foundry无需担心像Hardhat这类JavaScript环境会产生任何额外的gas费用。

在Moonbase Alpha上部署

在本教程中,我们将使用Foundry的脚本部署MyTokenContainer智能合约。要创建部署脚本,在script文件夹中创建一个新文件:

cd script
touch Container.s.sol

按照惯例,脚本应该以s.sol结尾,并且名称要与相关的脚本相似。在本示例中,我们要部署Container智能合约,因此我们将脚本命名为Container.s.sol,但如果您使用其他合适的或更具描述性的名称,也不会是世界末日。

在脚本中,添加以下内容:

pragma solidity ^0.8.0;

import "forge-std/Script.sol";
import {MyToken} from "../src/MyToken.sol";
import {Container} from "../src/Container.sol";

contract ContainerDeployScript is Script {
    // Runs the script; deploys MyToken and Container
    function run() public {
        // Get the private key from the .env
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Make a new token
        MyToken token = new MyToken(1000);

        // Make a new container
        new Container(token, 500);

        vm.stopBroadcast();
    }
}

我们来分析一下此处的代码。第一行是标配:说明Solidity版本。导入内容包含之前添加的两个智能合约,我们即将部署这两个合约。导入Script合约包括了在脚本中使用的附加功能。

接下来,我们来看一下合约中的逻辑。只有一个函数run,脚本逻辑都在里面。在此run函数中,经常使用到vm对象。所有Forge cheatcode存储在里面,其决定了运行Solidity的虚拟机的状态。

run函数的第一行中,vm.envUint用于从系统环境变量获取私钥(我们将在后续完成此操作)。在第二行中,vm.startBroadcast开始播报,即表示后续逻辑会发生在链上。因此,当使用new关键字实例化MyTokenContainer合约时,这两个合约会在链上实例化。最后一行,vm.stopBroadcast结束播报。

在我们运行此脚本之前,先要设置一些环境变量。创建一个新的.env文件:

touch .env

在此文件中,添加以下内容:

PRIVATE_KEY=YOUR_PRIVATE_KEY
MOONSCAN_API_KEY=YOUR_MOONSCAN_API_KEY

注意事项

Foundry提供用于处理私钥的其他选项。根据个人选择决定是否在控制台使用该私钥,私钥是否存储在设备环境中,是否使用硬件钱包或者使用keystore。

要添加这些环境变量,请运行以下命令:

source .env

现在,您的脚本和项目已经可以准备部署了!使用以下命令进行操作:

forge script Container.s.sol:ContainerDeployScript --broadcast --verify -vvvv --rpc-url moonbase

此命令将ContainerDeployScript合约作为脚本运行。--broadcast选项告知Forge允许交易播报,--verify选项告知Forge在部署时向Moonscan验证,-vvvv使命令输出更详细,--rpc-url moonbase将网络设置为在foundry.toml中设置的moonbase

您将看到类似以下输出:

Running a Script in Foundry

您应该能够看到您的合约已成功部署并且已在Moonscan上得到验证。可以查看我部署Container.sol合约的地方。

您可以在GitHub上查看整个部署脚本。

在Moonbeam主网上部署

现在您对您的智能合约已经感到满意,并且想在Moonbeam主网上进行部署。此过程的操作与上述操作类似,因为您已在foundry.toml文件中添加了Moonbeam主网的信息,您只需将rpc-url从moonbase改成moonbeam即可:

forge script Container.s.sol:ContainerDeployScript --broadcast --verify -vvvv --rpc-url moonbeam

请注意,虽然这比较复杂,但是还有其他在Foundry中处理私钥的方法。 其中一些方法可以被认为比将生产私钥存储在环境变量中更安全。

这样就可以了!您已经从无到有,完成了一个完全经过测试、部署和验证的Foundry项目。现在您可以稍作调整将Foundry用于您自己的项目!

本教程仅用于教育目的。 因此,不应在生产环境中使用本教程中创建的任何合约或代码。

本网站的所有信息由第三方提供,仅供参考之用。Moonbeam文档网站(https://docs.moonbeam.network/)上列出和描述的任何项目与Moonbeam立场无关。Moonbeam Foundation不保证网站信息的准确性、完整性或真实性。如使用或依赖本网站信息,需自行承担相关风险,Moonbeam Foundation不承担任何责任和义务。这些材料的所有陈述和/或意见由提供方个人或实体负责,与Moonbeam Foundation立场无关,概不构成任何投资建议。对于任何特定事项或情况,应寻求专业权威人士的建议。此处的信息可能会包含或链接至第三方提供的信息与/或第三方服务(包括任何第三方网站等)。这类链接网站不受Moonbeam Foundation控制。Moonbeam Foundation对此类链接网站的内容(包括此类链接网站上包含的任何信息或资料)概不负责也不认可。这些链接内容仅为方便访客而提供,Moonbeam Foundation对因您使用此信息或任何第三方网站或服务提供的信息而产生的所有责任概不负责。
Last update: September 28, 2023
| Created: March 29, 2023