Foundry 的优势是完全使用 Solidity 进行开发与测试,且Foundry 构建、测试的执行速度非常快。

对比HardHat

维度 Foundry Hardhat
开发语言 纯 Solidity(测试、部署、脚本都用 Solidity) JavaScript/TypeScript(测试、脚本)+ Solidity(合约)
学习曲线 对 Solidity 开发者更友好,上手快 需掌握 JS/TS,适合全栈背景开发者
测试效率 原生支持多线程,测试速度极快 基于 Node.js,测试速度较快但略逊于 Foundry
生态集成 生态较新,工具链集成度高(铸币、Fuzz 测试等) 生态成熟,插件丰富(如 Hardhat Etherscan、Hardhat Deploy)
部署流程 部署脚本用 Solidity 编写,简洁直接 部署脚本用 JS/TS,灵活性高
Fuzz 测试 原生支持,内置模糊测试工具 需依赖第三方插件(如hardhat-fuzz
调试体验 调试功能较弱,主要依赖日志输出 内置调试器,支持断点、单步执行
适用场景 适合纯 Solidity 项目、追求极致测试速度 适合复杂项目、需要丰富插件生态、或全栈开发

开始

安装Foundry

如果你是windows系统,可能要去Git Bash里面输入以下命令。

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

初始化

1
2
cd <你的项目所在的文件夹名称>
forge init <你的项目名字>

初始化在forge init <你的项目名字>中,init命令会自动安装forge-std库,如果安装失败(在lib文件夹里面查看是否有forge-std文件),可以手动输入以下命令。
forge install foundry-rs/forge-std

编译合约

输入命令forge build来编译src目录下的所有合约:

1
2
3
4
5
6
7
D:\AllCode\Solidity\Foundry\Program\Start01\start01>forge build
[⠊] Compiling...
[⠒] Compiling 23 files with Solc 0.8.30
[⠊] Installing Solc version 0.8.30
[⠰] Successfully installed Solc 0.8.30
[⠒] Solc 0.8.30 finished in 8.27s
Compiler run successful!

测试合约

输入命令forge test来编译test目录下的所有合约测试:

1
2
3
4
5
6
7
8
9
10
D:\AllCode\Solidity\Foundry\Program\Start01\start01>forge test
[⠒] Compiling...
No files changed, compilation skipped

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 28978, ~: 29289)
[PASS] test_Increment() (gas: 28783)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 19.45ms (17.78ms CPU time)

Ran 1 test suite in 29.45ms (19.45ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

如果想要获得更详细的测试:forge test -vvvv

部署合约

首先处理好隐私数据.envfoundry.toml文件,可以先编写好脚本文件,然后在Git Bash里输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ forge script script/Counter.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --broadcast

[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.

## Setting up 1 EVM.

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

Chain 11155111

Estimated gas price: 0.00110002 gwei

Estimated total gas used for script: 203856

Estimated amount required: 0.00000022424567712 ETH

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

##### sepolia
✅ [Success] Hash: 0xd0c5ee39c0360c5d7c6e7a72f2403b18a5e2a5d635c9ba8a4c8b67fe5259d69d
Contract Address: 0xb096AaAe91f89CC344080E2Af798fdb9F07DD92E
Block: 9556366
Paid: 0.000000172495240878 ETH (156813 gas * 0.001100006 gwei)

✅ Sequence #1 on sepolia | Total Paid: 0.000000172495240878 ETH (156813 gas * avg 0.001100006 gwei)


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

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Transactions saved to: D:/AllCode/Solidity/Foundry/Program/Start01/start01/broadcast\Counter.s.sol\11155111\run-latest.json

Sensitive values saved to: D:/AllCode/Solidity/Foundry/Program/Start01/start01/cache\Counter.s.sol\11155111\run-latest.json

OK,部署成功!

我在cmd和powershell里面输入命令去部署会失败,建议在GitBash里面输入命令。因为Git Bash 一般会自动读取.env文件中的变量,且支持 $变量名 格式。

如果Git Bash 无法读取.env文件中的变量,尝试手动输入:$ source .env
然后查看是否读取:$ echo $SEPOLIA_RPC_URL(私钥同理)

提示:.env文件里的等号两边似乎不能有空格。

隐私数据的处理

在项目根目录下添加.env文件,写入关键信息:

1
2
PRIVATE_KEY=0x(钱包私钥)
SEPOLIA_RPC_URL=https://

foundry.toml文件里添加:

1
2
3
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
local = "http://127.0.0.1:8545"

编写测试合约

solidity语言

导入Test.sol

1
import "forge-std/Test.sol";

Test.sol 是 Foundry 提供的测试基类,它包含:

  1. 断言函数(用于验证结果):
    • assertEq(a, b) - 断言 a 等于 b
    • assertGt(a, b) - 断言 a 大于 b
    • assertTrue(condition) - 断言条件为真
  2. 作弊码(Cheatcodes):通过vm对象提供:
    • vm.deal() -> 给地址发钱
    • vm.prank() -> 切换某地址调用一次
    • vm.startPrank() / vm.stopPrank() - 持续模拟某地址调用
作弊码 作用 示例
vm.deal(地址, 金额) 给地址发 ETH vm.deal(alice, 100 ether)
vm.prank(地址) 下一次调用模拟该地址 vm.prank(alice); pool.deposit();
vm.startPrank(地址) 持续模拟该地址 vm.startPrank(alice); ...
vm.stopPrank() 停止模拟 vm.stopPrank();
vm.warp(时间戳) 设置区块时间 vm.warp(1234567890);
vm.roll(块号) 设置区块号 vm.roll(100);
vm.expectRevert() 期望下一次调用回滚 vm.expectRevert(); pool.withdraw();

Foundry 规则:测试合约必须继承Test,继承后,你就可以使用vmassertEq等功能

初始化

setUp() 是特殊函数,每个测试函数运行前都会自动执行,类似于其他测试框架的 beforeEach(),用于初始化测试环境。

1
2
3
4
5
6
7
8
9
10
11
    /**
     * @notice 部署资金池并注入 1000 ETH
     */
    function setUp() public {
        pool = new ShadowLendPool();

        vm.deal(address(this), 1000 ether);
        pool.deposit{value: 1000 ether}();
        // 给玩家一点 ETH 启动资金
        vm.deal(player, 1 ether);
    }

new 关键字用于部署新合约,部署者是 address(this)(测试合约本身)。部署后,pool 变量指向新合约的地址

等价于在JavaScript中: const pool = await deployer.deploy(ShadowLendPool);

vm是Foundry提供的全局对象,包含各种”作弊”功能,可以操纵区块链状态,只在测试环境有效,真实链上无法使用

vm.deal(地址, 金额)的作用:在真实区块链上,你需要有 ETH 才能转账。在测试中,可以用 vm.deal() 凭空创造 ETH,这样测试更方便,不需要真实的资金。

测试函数

1
2
3
4
5
6
7
8
9
10
11
    function test_exploit() public {
        assertEq(address(pool).balance, 1000 ether);       assertEq(pool.balances(player), 0);

        vm.startPrank(player);
        AttackShadowLend attackContract = new AttackShadowLend(pool);
        attackContract.attack{value: 1 ether}();
        vm.stopPrank();
       
        assertEq(address(pool).balance, 0);
        assertGt(player.balance, 1000 ether);
    }

Foundry 规则:测试函数必须以 test 开头,Foundry 会自动识别并运行这些函数

  • ssertEq(a, b) 的作用是断言 a == b;如果不相等,测试失败并抛出错误。
  • vm.startPrank(player) 的作用是从现在开始,所有函数调用都会”假装”是 player 发起的,即msg.sender == player
  • assertGt(a, b) 的作用是断言 a > b(greater than);如果 a <= b,测试失败。

JavaScript语言

详见《合约测试》笔记