使用foundry实现部署你的第一个智能合约

295 阅读13分钟

参考资料

foundry doc ---> book.getfoundry.sh/introductio…

使用foundry实现你的第一个智能合约

目录

  1. Foundry 简介
  2. 工具对比
  3. 环境配置
  4. 项目结构
  5. 测试指南
  6. 部署流程
  7. Cast 操作合约
  8. Chisel - Solidity REPL 工具
  9. 总结

什么是 Foundry?

Foundry 是一个用于以太坊智能合约开发的高性能工具链。它由 Rust 编写,包含了合约编译、测试、部署、交互等一整套开发流程的命令行工具,主要包括:

核心组件

  • forge:编译、测试、部署智能合约
  • cast:链上交互、数据查询、发送交易
  • anvil:本地以太坊节点模拟器
  • chisel:Solidity REPL

与 Remix、Hardhat 的区别

工具主要特点适用场景
RemixWeb IDE,零配置,适合新手和演示快速原型、教学
HardhatJS/TS 生态,插件丰富,脚本灵活前端集成、复杂脚本
FoundryCLI 工具链,极致性能,原生支持 Solidity 脚本高效测试、CI、Solidity
  • 开发体验
    • Remix 是网页 IDE,零配置,适合入门和小项目。
    • Hardhat 以 JS/TS 为主,适合与前端集成,生态丰富。
    • Foundry 完全基于命令行,配置简单,极致性能,适合大规模测试和 CI。
  • 测试方式
    • Remix 支持简单的合约交互和测试。
    • Hardhat 测试用 JS/TS 编写。
    • Foundry 测试用 Solidity 编写,支持 fuzzing、property-based testing。
  • 性能
    • Foundry 的测试和编译速度极快,适合大项目和持续集成。
  • 生态集成
    • Hardhat 插件生态丰富,适合与 JS/TS 项目集成。
    • Foundry 支持与 Hardhat 项目互操作(可共用合约、测试等)。

为什么要用foundry?Foundry 的优势?

  1. 极致性能
    • 编译和测试速度极快,适合大规模项目和 CI/CD。
  2. Solidity 原生测试
    • 测试用 Solidity 编写,支持 fuzzing、property-based testing。
  3. 易于集成
    • 命令行工具,易于脚本化和自动化。
  4. 本地节点(Anvil)
    • 内置本地节点,方便调试和测试。
  5. 与 Hardhat 兼容
    • 可与 Hardhat 项目共用合约和依赖。 :

如何使用foundry

1. 安装 Foundry

在终端执行以下命令:

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

2. 初始化项目

forge init my-foundry-project
cd my-foundry-project
tree -L 1
tree src
tree test
tree script
.
├── cache
├── foundry.toml
├── lib
├── out
├── README.md
├── script
├── src
└── test

7 directories, 2 files
src
└── Counter.sol

1 directory, 1 file
test
└── Counter.t.sol

1 directory, 1 file
script
└── Counter.s.sol

3.Foundry 的核心工具与常用命令

Foundry 工具链包含 4 个主要工具,每个工具都专注于以太坊开发流程中的不同环节:

3.1 forge
  • 用途:合约的编译、测试、部署、脚本执行。
  • 常用命令
    • forge init <project-name>:初始化新项目
    • forge build:编译合约
    • forge test:运行 Solidity 测试
    • forge script <script>:运行 Solidity 脚本
    • forge create <contract>:部署合约
3.2 cast
  • 用途:与链交互、数据查询、发送交易、工具函数等。
  • 常用命令
    • cast call <address> <sig>:调用合约的 view/pure 方法
    • cast send <address> <sig>:发送交易
    • cast block <block>:查询区块信息
    • cast tx <hash>:查询交易信息
    • cast keccak <data>:计算 keccak256 哈希
3.3 anvil
  • 用途:本地以太坊节点模拟器,适合本地开发和测试。
  • 常用命令
    • anvil:启动本地节点
    • 支持自定义端口、账户、初始余额等参数
3.4 chisel
  • 用途:Solidity REPL(交互式命令行),用于快速实验和调试 Solidity 代码片段。
  • 常用命令
    • chisel:进入 REPL 交互环境
    • 在环境中直接输入 Solidity 代码片段进行测试

4.测试

forge test
[⠊] Compiling...
No files changed, compilation skipped

Ran 4 tests for test/Counter.t.sol:CounterTest
[PASS] testAdd() (gas: 32441)
[PASS] testFuzz_Add(uint256) (runs: 256, μ: 32416, ~: 32572)
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 32398, ~: 32398)
[PASS] test_Increment() (gas: 31895)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 16.43ms (19.59ms CPU time)

Ran 1 test suite in 160.68ms (16.43ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)

注意 测试的方法名必须是驼峰或下划线 以test开头

常用参数
  • -vv-vvv-vvvv 等:增加输出详细程度(如显示测试日志、trace 等)
  • --match-test <pattern>:只运行名称匹配的测试函数
  • --gas-report:输出 gas 消耗报告
  • --fork-url <RPC_URL>:在 fork 主网/测试网的环境下运行测试
示例
forge test -vvvvv //-v -vv -vvv -vvvv 测试严格程度依次增加
[⠊] Compiling...
No files changed, compilation skipped

Ran 4 tests for test/Counter.t.sol:CounterTest
[PASS] testAdd() (gas: 32441)
Traces:
  [32441] CounterTest::testAdd()
    ├─ [22689] Counter::add(10)
    │   └─ ← [Stop]
    ├─ [446] Counter::number() [staticcall]
    │   └─ ← [Return] 10
    ├─ [0] VM::assertEq(10, 10) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

[PASS] testFuzz_Add(uint256) (runs: 256, μ: 32416, ~: 32572)
Traces:
  [32572] CounterTest::testFuzz_Add(668)
    ├─ [22689] Counter::add(668)
    │   └─ ← [Stop]
    ├─ [446] Counter::number() [staticcall]
    │   └─ ← [Return] 668
    ├─ [0] VM::assertEq(668, 668) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 32242, ~: 32398)
Traces:
  [32398] CounterTest::testFuzz_SetNumber(25185550902188 [2.518e13])
    ├─ [22514] Counter::setNumber(25185550902188 [2.518e13])
    │   └─ ← [Stop]
    ├─ [446] Counter::number() [staticcall]
    │   └─ ← [Return] 25185550902188 [2.518e13]
    ├─ [0] VM::assertEq(25185550902188 [2.518e13], 25185550902188 [2.518e13]) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

[PASS] test_Increment() (gas: 31895)
Traces:
  [31895] CounterTest::test_Increment()
    ├─ [22440] Counter::increment()
    │   └─ ← [Stop]
    ├─ [446] Counter::number() [staticcall]
    │   └─ ← [Return] 1
    ├─ [0] VM::assertEq(1, 1) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 15.68ms (21.00ms CPU time)

Ran 1 test suite in 156.03ms (15.68ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
function testFuzz_Add_Boundary(uint256 x) public {
    // 只允许 x 为 0 或最大 uint256
    vm.assume(x == 0 || x == type(uint256).max);
    counter.setNumber(0);
    counter.add(x);
    assertEq(counter.number(), x);
}
forge test -vvvv --gas-report  --match-test testFuzz_Add_Boundary
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for test/Counter.t.sol:CounterTest
[PASS] testFuzz_Add_Boundary(uint256) (runs: 256, μ: 70263, ~: 63446)
Traces:
  [63446] CounterTest::testFuzz_Add_Boundary(0)
    ├─ [0] VM::assume(true) [staticcall]
    │   └─ ← [Return]
    ├─ [23806] Counter::setNumber(0)
    │   └─ ← [Stop]
    ├─ [23981] Counter::add(0)
    │   └─ ← [Stop]
    ├─ [2446] Counter::number() [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return]
    └─ ← [Stop]

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 129.50ms (122.97ms CPU time)

╭----------------------------------+-----------------+-------+--------+-------+---------╮
| src/Counter.sol:Counter Contract |                 |       |        |       |         |
+=======================================================================================+
| Deployment Cost                  | Deployment Size |       |        |       |         |
|----------------------------------+-----------------+-------+--------+-------+---------|
| 181883                           | 625             |       |        |       |         |
|----------------------------------+-----------------+-------+--------+-------+---------|
|                                  |                 |       |        |       |         |
|----------------------------------+-----------------+-------+--------+-------+---------|
| Function Name                    | Min             | Avg   | Median | Max   | # Calls |
|----------------------------------+-----------------+-------+--------+-------+---------|
| add                              | 23981           | 30795 | 23981  | 44265 | 256     |
|----------------------------------+-----------------+-------+--------+-------+---------|
| number                           | 2446            | 2446  | 2446   | 2446  | 256     |
|----------------------------------+-----------------+-------+--------+-------+---------|
| setNumber                        | 23806           | 23806 | 23806  | 23806 | 257     |
╰----------------------------------+-----------------+-------+--------+-------+---------╯


Ran 1 test suite in 145.92ms (129.50ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

注意 vm.assume 可控制边界

  • 运行所有测试,并显示详细日志和断言信息。
输出解读
  • 每个测试函数会显示 PASSFAIL,并统计通过/失败数量、总耗时等。
  • 失败时会显示断言失败的具体行号和原因。

5.本地RPC节点 anvil

anvil

6.部署

Foundry 的 forge create 命令可以直接将合约部署到以太坊主网或测试网或本地anvil。

基本命令格式
forge create <合约路径>:<合约名> --rpc-url <RPC_URL> --private-key <PRIVATE_KEY>
  • <合约路径>:合约文件的相对路径,如 src/Counter.sol
  • <合约名>:要部署的合约名,如 Counter
  • <RPC_URL>:以太坊节点的 RPC 地址(如 Infura、Alchemy、公共 RPC 等)
  • <PRIVATE_KEY>:用于部署的以太坊账户私钥
示例:部署到 Sepolia测试网或本地anvil节点

假设你要部署 src/Counter.sol 中的 Counter 合约:

forge create src/Counter.sol:Counter \
  --rpc-url https://sepolia.infura.io/v3/<YOUR_INFURA_PROJECT_ID> \
  --private-key <YOUR_PRIVATE_KEY>
常用参数
  • --constructor-args <args>:传递构造函数参数
  • --verify:部署后自动在 Etherscan 验证合约
  • --etherscan-api-key <API_KEY>:用于合约验证的 Etherscan API Key
keystore方式
cast wallet import -i test_account1  //输入私钥 输入密码加密
cast wallet address --account test_account1   //查看
 cat ~/.foundry/keystores/test_account1    
chain id
cast chain-id --rpc-url https://sepolia.infura.io/v3/你的APIKEY

11155111

注意 sepolia.infura.io/v3/你的APIKEYdeveloper.metamask.io 获取Sepolia的infura RPC 或者chainlist.org/ 寻找免费节点

.env
# sepolia
# ETH_RPC_URL=https://sepolia.infura.io/v3/你的APIKEY
# ETH_FROM=你的ADDRESS
# CHAIN=11155111
# ETH_KEYSTORE_ACCOUNT=test_account1

# local
ETH_KEYSTORE_ACCOUNT=local_account1
ETH_FROM=你的anvil_address
forge create -h|grep env
部署anvil
forge create src/Counter.sol:Counter --broadcast
[⠊] Compiling...
No files changed, compilation skipped
Enter keystore password:
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb9xxxx
Deployed to: 0x5FbDB2315678afecb367f032d93F642f641xxxx
Transaction hash: 0xad70bb57c7cad1fc9c13aa8619c7613d976a89c0964e8c4555710ef3xxxxxxxx

注意

1.部署前使用forge create src/Counter.sol:Counter不加广播(--broadcast)进行模拟部署不会上链

2.anvil本地节点部署在.env中只需要本地节点的ETH_KEYSTORE_ACCOUNT和ETH_FROM,

部署sepolia
# sepolia
ETH_RPC_URL=https://sepolia.infura.io/v3/你的APIKEY
ETH_FROM=你的address
CHAIN=11155111
ETH_KEYSTORE_ACCOUNT=test_account1
VERIFIER_API_KEY=你的etherscanAPIKey
# local
# ETH_KEYSTORE_ACCOUNT=local_account1
# ETH_FROM=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
forge create --account test_account1 Counter --broadcast
[⠊] Compiling...
No files changed, compilation skipped
Enter keystore password:
Deployer: 0xeeb0F1Dc21d47B81545c923523eE345002A2E5C0
Deployed to: 0x2F29BEc88245e3a334e94d6fdE27000f9b8407cE
Transaction hash: 0x3d71ee0c76eeadaba0a9b5b8b6a8aa98b814c981dcf7adfebec35628e3a747e9
验证开源
forge v 合约地址 Counter
Submitted contract for verification:
Response: `OK`
GUID: `ntyadhwyse3m6c82hwmt3zagza4hdfwcleg3f1mj1lusnahkma`
URL: https://sepolia.etherscan.io/address/0x2f29bec88245e3a334e94d6fde27000f9b8407ce

注意 需要申请区块链浏览器的apikey 并写入环境变量VERIFIER_API_KEY

截屏2025-06-04 21.30.57.png

注意 等待verified绿色 出现源码

脚本部署

注意 使用脚本部署依然依赖于你的环境变量.env,使用forge script -h|grep -Ei "ETH_RPC_URL|ETH_FROM|CHAIN|ETH_KEYSTORE_ACCOUNT|VERIFIER_API_KEY" 获取检查你的环境变量,脚本部署与命令行部署区别不大,脚本部署可以一次部署多个合约,并且可以编排检查

contract CounterScript is Script {
    Counter public counter;

    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        counter = new Counter();

        vm.stopBroadcast();
    }
}
forge script script/Counter.s.sol:CounterScript --rpc-url https://sepolia.infura.io/v3/你的APIKEY

注意 先不广播 先模拟 可检查gas消耗

forge script script/Counter.s.sol:CounterScript --rpc-url https://sepolia.infura.io/v3/你的APIKEY
[⠊] Compiling...
No files changed, compilation skipped
Enter keystore password:
Script ran successfully.

## Setting up 1 EVM.

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

Chain 11155111

Estimated gas price: 0.033585386 gwei

Estimated total gas used for script: 236447

Estimated amount required: 0.000007941163763542 ETH

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

真实部署加验证开源

forge script script/Counter.s.sol:CounterScript --rpc-url https://sepolia.infura.io/v3/你的apikey --broadcast  --verify -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Enter keystore password:
Traces:
  [160270] CounterScript::run()
    ├─ [0] VM::startBroadcast()
    │   └─ ← [Return]
    ├─ [119563] → new Counter@0xbDf202A319868310FEaBd0b18A4306491832AF7c
    │   └─ ← [Return] 597 bytes of code
    ├─ [0] VM::stopBroadcast()
    │   └─ ← [Return]
    └─ ← [Stop]


Script ran successfully.

## Setting up 1 EVM.
==========================
Simulated On-chain Traces:

  [119563] → new Counter@0xbDf202A319868310FEaBd0b18A4306491832AF7c
    └─ ← [Return] 597 bytes of code


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

Chain 11155111

Estimated gas price: 0.065441444 gwei

Estimated total gas used for script: 236447

Estimated amount required: 0.000015473433109468 ETH

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

##### sepolia
✅  [Success] Hash: 0x3df28f7c798a51e39ba667987a224cdc66782a6273c0ace8fa6fc7675d887394
Contract Address: 0xbDf202A319868310FEaBd0b18A4306491832AF7c
Block: 8475622
Paid: 0.000006333707343808 ETH (181883 gas * 0.034822976 gwei)

✅ Sequence #1 on sepolia | Total Paid: 0.000006333707343808 ETH (181883 gas * avg 0.034822976 gwei)
                                                                                                                                                                

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

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
##
Start verification for (1) contracts
Start verifying contract `0xbDf202A319868310FEaBd0b18A4306491832AF7c` deployed on sepolia
EVM version: cancun
Compiler version: 0.8.30

Submitting verification for [src/Counter.sol:Counter] 0xbDf202A319868310FEaBd0b18A4306491832AF7c.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (4 tries remaining)

Submitting verification for [src/Counter.sol:Counter] 0xbDf202A319868310FEaBd0b18A4306491832AF7c.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (3 tries remaining)

Submitting verification for [src/Counter.sol:Counter] 0xbDf202A319868310FEaBd0b18A4306491832AF7c.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (2 tries remaining)

Submitting verification for [src/Counter.sol:Counter] 0xbDf202A319868310FEaBd0b18A4306491832AF7c.
Warning: Could not detect the deployment.; waiting 5 seconds before trying again (1 tries remaining)

Submitting verification for [src/Counter.sol:Counter] 0xbDf202A319868310FEaBd0b18A4306491832AF7c.
Submitted contract for verification:
        Response: `OK`
        GUID: `snbeuz71u1bsp3n8ms8rxe4tipwbcqbjcu5nn3qmkhjhravndm`
        URL: https://sepolia.etherscan.io/address/0xbdf202a319868310feabd0b18a4306491832af7c
Contract verification status:
Response: `NOTOK`
Details: `Already Verified`
Contract source code already verified
All (1) contracts were verified!

7. Cast 操作合约

7.1 基础查询命令

网络状态查询

# 查询当前网络 Chain ID
cast chain-id --rpc-url $RPC_URL

# 查询最新区块号
cast block-number --rpc-url $RPC_URL

# 查询 Gas 价格
cast gas-price --rpc-url $RPC_URL

7.2 合约交互

查询合约状态

# 查询合约变量
cast call $CONTRACT_ADDRESS "number()" --rpc-url $RPC_URL

# 查询合约字节码
cast code $CONTRACT_ADDRESS --rpc-url $RPC_URL

# 查询合约余额
cast balance $CONTRACT_ADDRESS --rpc-url $RPC_URL

发送交易

# 调用合约方法(需要私钥)
cast send $CONTRACT_ADDRESS "increment()" \
  --private-key $PRIVATE_KEY \
  --rpc-url $RPC_URL

# 带参数的合约调用
cast send $CONTRACT_ADDRESS "setNumber(uint256)" 100 \
  --private-key $PRIVATE_KEY \
  --rpc-url $RPC_URL

7.3 交易信息查询

交易详情

# 查询交易信息
cast tx $TX_HASH --rpc-url $RPC_URL

# 查询交易收据
cast receipt $TX_HASH --rpc-url $RPC_URL

# 追踪交易执行
cast trace $TX_HASH --rpc-url $RPC_URL

7.4 实用工具

数据转换

# ABI 编码
cast abi-encode "transfer(address,uint256)" $ADDRESS 100

# 计算函数选择器
cast sig "transfer(address,uint256)"

# 解码交易输入数据
cast 4byte $TX_INPUT_DATA

8. Chisel - Solidity REPL 工具

8.1 什么是 Chisel?

Chisel 是 Foundry 工具链中的 Solidity REPL(Read-Eval-Print Loop)工具,它允许你:

  • 快速测试 Solidity 代码片段
  • 实时验证合约逻辑
  • 学习和实验 Solidity 语法
  • 调试合约功能

8.2 基本使用

启动 Chisel

# 进入 Chisel REPL 环境
chisel

# 你会看到类似这样的提示符

基本操作示例

// 声明变量
uint256 x = 1;

// 定义函数
function add(uint256 a, uint256 b) pure returns (uint256) {
    return a + b;
}

// 调用函数
add(1, 2)

8.3 高级功能

导入外部合约
// 导入 OpenZeppelin 合约
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// 创建合约实例
contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {}
}

9. 总结与最佳实践

9.1 完整开发流程回顾

从零到部署的完整流程

  1. 环境准备

    • 安装 Foundry:curl -L https://foundry.paradigm.xyz | bash
    • 更新工具链:foundryup
  2. 项目初始化

    • 创建项目:forge init my-foundry-project
    • 目录结构:src/(合约)、test/(测试)、script/(部署脚本)
  3. 合约开发

    • 编写智能合约(如 Counter.sol)
    • 使用 forge build 编译
    • 使用 forge test 运行测试
  4. 本地测试

    • 启动本地节点:anvil
    • 运行测试:forge test -vvvv
    • 模拟部署:forge script script/Counter.s.sol:CounterScript
  5. 正式部署

    • 配置环境变量(.env)
    • 准备部署脚本
    • 执行部署:forge script script/Counter.s.sol:CounterScript --broadcast --verify
  6. 合约验证

    • 确认部署成功
    • 验证合约源码
    • 在区块浏览器查看
  7. 合约交互

    • 使用 cast 查询状态
    • 发送交易调用函数
    • 监控交易状态