安装

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

运行 foundryup 将自动安装最新的(夜间)版本的 预编译二进制文件:forge、cast、anvil 和 chisel。有关其他选项,例如安装特定版本或提交,可以运行 foundryup --help 查看

项目使用

  • 新建项目
$ forge init new_project
// 指定模板创建
$ forge init --template https://github.com/foundry-rs/forge-template new_project
  • 构建 $ forge build
  • 安装依赖 $ forge install xxx
  • 删除依赖 $ forge remove xxx
  • 依赖映射 $ forge remappings 或者新建修改 remapping.txt 文件
  • 部署 $ forge create --rpc-url <your_rpc_url> --private-key <your_private_key> src/MyContract.sol:MyContract
  • 克隆链上已验证的合约 $ forge clone 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 WETH9

脚本部署

  1. 编写部署脚本
// script/DeployCounter.s.sol
pragma solidity ^0.8.0;

import "forge-std/Script.sol";
import "../src/Counter.sol";

contract DeployCounter is Script {
    function run() external {
        vm.startBroadcast(); // 开始广播交易(部署合约)

        // 部署合约
        Counter counter = new Counter();

        console.log("Counter deployed at:", address(counter));

        vm.stopBroadcast(); // 停止广播交易
    }
}
  1. 模拟部署(运行脚本但不发送交易)
forge script script/DeployCounter.s.sol:DeployCounter --fork-url http://127.0.0.1:8545 --broadcast --dry-run
  1. 实际部署
forge script script/DeployCounter.s.sol:DeployCounter --rpc-url http://127.0.0.1:8545 --broadcast --private-key YOUR_PRIVATE_KEY

常用参数说明 --rpc-url: 指定连接的网络节点(本地 Anvil 或测试网/主网)。 --private-key: 提供部署合约所需的钱包私钥。 --broadcast: 执行真实的部署交易。 --dry-run: 模拟运行脚本,不发送交易。 --etherscan-api-key: 部署到主网后用于合约验证

部署最佳实践

  1. 分离部署逻辑:将复杂的部署逻辑写成多个小脚本,例如一个脚本用于部署库,另一个脚本部署主合约。
  2. 记录部署结果:使用日志记录合约地址(如 console.log),或者将结果写入文件以便后续使用。
  3. 模拟部署:在主网或测试网进行真实部署前,先使用 –dry-run 模拟运行,确保脚本无误。
  4. 切勿将私钥直接写入脚本中,可以使用环境变量传递

测试

基本测试

pragma solidity 0.8.10;

import {Test} from "forge-std/Test.sol";

contract ContractBTest is Test {
    uint256 testNumber;

    // 测试前的初始化设置
    function setUp() public {
        testNumber = 42;
    }

    // 正常测试
    function test_NumberIs42() public {
        assertEq(testNumber, 42);
    }

    // 测试合约异常,必须抛出异常被捕获测试才能通过
    function testFail_Subtract43() public {
        vm.expectRevert(xxx);
    }
}

模糊测试

contract SafeTest is Test {
    // amount 随机生成
    function testFuzz_Withdraw(uint256 amount) public {
        // 设置随机数范围
        vm.assume(amount > 0.1 ether);
        payable(address(safe)).transfer(amount);
        uint256 preBalance = address(this).balance;
        safe.withdraw();
        uint256 postBalance = address(this).balance;
        assertEq(preBalance + amount, postBalance);
    }
}

不变测试

不变量测试允许对预定义合约中的预定义函数调用序列进行随机化测试,以验证一组不变量表达式, 由于函数调用序列是随机化的,并且输入是模糊的,不变量测试可以在边缘情况和高度复杂的协议状态中揭示错误的假设和不正确的逻辑

// 不变量测试通过在函数名称前加上 invariant 来表示
function invariant_example() external {
    assertEq(val1, val2);
}

分叉测试

// 在分叉环境(例如分叉的以太坊主网)中运行所有测试
forge test --fork-url <your_rpc_url> --fork-block-number 19088

或者编写测试代码, 创建和选择分叉

contract ForkTest is Test {
    // the identifiers of the forks
    uint256 mainnetFork;
    uint256 optimismFork;

    // create two _different_ forks during setup
    function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        optimismFork = vm.createFork(OPTIMISM_RPC_URL);
    }

    // demonstrate fork ids are unique
    function testForkIdDiffer() public {
        assert(mainnetFork != optimismFork);
    }

    function testCanSwitchForks() public {
        // select a specific fork
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);

         // manage multiple forks in the same test
        vm.selectFork(optimismFork);
        assertEq(vm.activeFork(), optimismFork);
    }

    // forks can be created at all times
    function testCanCreateAndSelectForkInOneStep() public {
        // creates a new fork and also selects it
        uint256 anotherFork = vm.createSelectFork(MAINNET_RPC_URL);
        assertEq(vm.activeFork(), anotherFork);
        // set `block.number` of a fork
        vm.rollFork(1_337_000);
        assertEq(block.number, 1_337_000);
    }
}

常用测试命令

// 测试合约
forge test --mc TestContract  -vv 
// 测试某个函数方法
forge test --mt test_Method  -vv 
// 测试并生成 Gas 报告
forge test --gas-report
// 省略成功的测试,仅重放记录的失败
forge test --rerun

一些测试规范

  1. 保持测试命名一致
test_Description 用于标准测试的。
testFuzz_Description 用于模糊测试。
test_Revert[If|When]_Condition 用于期望 revert 的测试。
testFork_Description 用于从网络分叉的测试。
testForkFuzz_Revert[If|When]_Condition 用于分叉并期望 revert 的模糊测试
  1. 永远不要在 setUp 函数中做出断言,如果你需要确保 setUp 函数执行预期的操作,请使用像 test_SetUpState() 这样的专用测试
  2. 相关代码应彼此靠近放置

与 Hardhat 兼容

  1. 初始化 foundry 项目, 修改 foundry.toml 文件,将默认的合约目录(src/)改为与 Hardhat 兼容的 contracts/
[default]
src = 'contracts'
out = 'out'
libs = ['lib']
  1. 安装并初始化 hardhat
npm init -y
npm install --save-dev hardhat
npx hardhat
  1. 修改 hardhat.config.js 文件以支持 Foundry 的 contracts/ 目录
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config(); // 支持环境变量

module.exports = {
  solidity: "0.8.19", // 版本与 Foundry 配置保持一致
  paths: {
    sources: "./contracts", // 指定与 Foundry 共享的合约目录
    artifacts: "./artifacts", // 输出目录
  },
  networks: {
    hardhat: {}, // 本地 Hardhat 网络
    localhost: {
      url: "http://127.0.0.1:8545", // 连接 Anvil
    },
    goerli: {
      url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};
  1. 将 Foundry 和 Hardhat 集成在一起的项目结构如下:
my-project/
├── contracts/                # Solidity 源代码目录(共享)
│   ├── MyContract.sol
├── foundry.toml              # Foundry 配置文件
├── hardhat.config.js         # Hardhat 配置文件
├── lib/                      # Foundry 库
├── scripts/                  # Hardhat 部署脚本目录
│   ├── deploy.js
├── test/                     # 测试代码(Solidity 和 JavaScript 混合)
│   ├── foundry/              # Foundry 测试目录
│   │   ├── MyContract.t.sol
│   ├── hardhat/              # Hardhat 测试目录
│       ├── test.js

工具包

  • Cast: Foundry 用于执行以太坊 RPC 调用的命令行工具。 你可以进行智能合约调用、发送交易或检索任何类型的链数据
  • Anvil: 创建一个本地测试网节点,用于部署和测试智能合约, 也可以用来分叉其他与 EVM 兼容的网络
  • Chisel: 用于在本地或分叉网络上快速测试 Solidity 片段

参考

  1. Foundry Book: Introduction
  2. foundry 中文文档