- Published on
foundry项目基本构建
- Authors

- Name
- 卢翔宇
- @y9840836216317
foundry项目基本构建
新建一个项目
forge init
├── README.md
├── foundry.toml
├── lib
│
├── script
│ └── Counter.s.sol
├── src
│ └── Counter.sol
└── test
└── Counter.t.sol
lib下是项目依赖,src下是项目主文件,script下是项目脚本文件,test下是项目测试文件。
安装依赖
forge install OpenZeppelin/openzeppelin-contracts
实例
我们将创建一个简单的存储合约 src/SimpleStorage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 从 OpenZeppelin 库导入 Ownable 合约
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title SimpleStorage
* @dev 一个用于演示基本功能的简单存储合约。
*/
contract SimpleStorage is Ownable {
uint256 public number;
string public message;
event NumberChanged(uint256 indexed newNumber, address indexed changedBy);
event MessageChanged(string indexed newMessage, address indexed changedBy);
/**
* @dev 设置初始的 number, message, 和 owner。
* @param _initialNumber 初始数字。
* @param _initialMessage 初始信息。
*/
constructor(uint256 _initialNumber, string memory _initialMessage) Ownable(msg.sender) {
number = _initialNumber;
message = _initialMessage;
}
/**
* @dev 设置一个新的数字。
* @param _newNumber 要存储的新数字。
*/
function setNumber(uint256 _newNumber) public {
number = _newNumber;
emit NumberChanged(_newNumber, msg.sender);
}
/**
* @dev 设置一条新信息。只有 owner 可以调用此函数。
* @param _newMessage 要存储的新信息。
*/
function setMessage(string memory _newMessage) public onlyOwner {
require(bytes(_newMessage).length > 0, "Message cannot be empty");
message = _newMessage;
emit MessageChanged(_newMessage, msg.sender);
}
}
test/SimpleStorage.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {SimpleStorage} from "src/SimpleStorage.sol";
contract SimpleStorageTest is Test {
SimpleStorage public simpleStorage;
address public owner = address(this); // 测试合约本身是 owner
address public user = makeAddr("user"); // 创建一个虚拟的用户地址
uint256 private constant INITIAL_NUMBER = 5;
string private constant INITIAL_MESSAGE = "Hello Foundry";
/**
* @dev 在每个测试用例运行前执行,用于设置初始状态。
*/
function setUp() public {
// 部署一个新的 SimpleStorage 合约实例
simpleStorage = new SimpleStorage(INITIAL_NUMBER, INITIAL_MESSAGE);
}
// ==================================
// Unit Tests
// ==================================
/**
* @dev 测试合约部署后的初始状态是否正确。
*/
function test_InitialState() public {
assertEq(simpleStorage.number(), INITIAL_NUMBER, "Initial number should be correct");
assertEq(simpleStorage.message(), INITIAL_MESSAGE, "Initial message should be correct");
assertEq(simpleStorage.owner(), owner, "Owner should be the test contract");
}
/**
* @dev 测试 setNumber 函数是否能成功更新数字并发出事件。
*/
function test_SetNumber() public {
uint256 newNumber = 100;
// 预期会发出一个 NumberChanged 事件
vm.expectEmit(true, true, false, true);
emit SimpleStorage.NumberChanged(newNumber, address(this));
// 调用函数
simpleStorage.setNumber(newNumber);
// 断言状态已更新
assertEq(simpleStorage.number(), newNumber, "Number should be updated");
}
/**
* @dev 测试 owner 是否能成功更新 message。
*/
function test_SetMessage_AsOwner() public {
string memory newMessage = "Hello World";
// 预期会发出一个 MessageChanged 事件
vm.expectEmit(true, true, false, true);
emit SimpleStorage.MessageChanged(newMessage, address(this));
simpleStorage.setMessage(newMessage);
assertEq(simpleStorage.message(), newMessage, "Message should be updated by owner");
}
/**
* @dev 测试当非 owner 调用 setMessage 时交易是否会回滚。
*/
function test_RevertWhen_SetMessageByNonOwner() public {
// 切换调用者身份为 "user"
vm.startPrank(user);
// 预期交易会因为 Ownable 错误而回滚
vm.expectRevert(bytes("Ownable: caller is not the owner"));
simpleStorage.setMessage("malicious message");
// 停止模拟
vm.stopPrank();
}
/**
* @dev 测试设置空消息时交易是否会回滚。
*/
function test_RevertWhen_SetEmptyMessage() public {
// 预期交易会因为我们自定义的 require 错误而回滚
vm.expectRevert(bytes("Message cannot be empty"));
simpleStorage.setMessage("");
}
// ==================================
// Fuzz Tests
// ==================================
/**
* @dev 使用随机输入对 setNumber 函数进行模糊测试。
*/
function testFuzz_SetNumber(uint256 _number) public {
simpleStorage.setNumber(_number);
assertEq(simpleStorage.number(), _number, "Fuzz test failed for setNumber");
}
/**
* @dev 使用随机输入和随机调用者对 setNumber 进行模糊测试。
* actorSeed 用于从一组预定义地址中选择一个模拟调用者。
*/
function testFuzz_SetNumber_WithRandomActor(uint256 _number, uint256 actorSeed) public {
// 限制 actorSeed 在 0 和 1 之间,以在 owner 和 user 之间切换
actorSeed = bound(actorSeed, 0, 1);
address actor = actorSeed == 0 ? owner : user;
vm.prank(actor);
// 预期事件
vm.expectEmit(true, true, false, true);
emit SimpleStorage.NumberChanged(_number, actor);
simpleStorage.setNumber(_number);
assertEq(simpleStorage.number(), _number, "Fuzz test with actor failed");
}
}
script/DeploySimpleStorage.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console} from "forge-std/Script.sol";
import {SimpleStorage} from "src/SimpleStorage.sol";
contract DeploySimpleStorage is Script {
function run() public returns (address) {
// 从环境变量中获取部署者的私钥
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// 启动广播,意味着接下来的交易将被发送到链上
vm.startBroadcast(deployerPrivateKey);
// 部署合约,传入构造函数参数
SimpleStorage simpleStorage = new SimpleStorage(10, "Initial Deploy");
// 停止广播
vm.stopBroadcast();
// 在控制台打印出已部署的合约地址,这非常重要!
console.log("SimpleStorage contract deployed to:", address(simpleStorage));
return address(simpleStorage);
}
}
编译项目
forge build
进行测试
forge test
创建本地虚拟链
anvil
anvil会生成十个虚拟地址及其私钥
脚本运行
export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
forge script script/DeploySimpleStorage.s.sol --rpc-url http://127.0.0.1:8545 --broadcast
我们把anvil生成的第一个地址的私钥存放到环境变量中,再使用forge script运行脚本(8545是anvil虚拟链运行的默认端口)
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
== Return ==
0: address 0x5FbDB2315678afecb367f032d93F642f64180aa3
== Logs ==
SimpleStorage contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
## Setting up 1 EVM.
==========================
Chain 31337
Estimated gas price: 2.000000001 gwei
Estimated total gas used for script: 1045383
Estimated amount required: 0.002090766001045383 ETH
==========================
##### anvil-hardhat
✅ [Success] Hash: 0x677e4d8c093176d255733c42b1392449431b95c5b56a7f40f5b015b2a4c6c4c5
Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Block: 1
Paid: 0.000804141000804141 ETH (804141 gas * 1.000000001 gwei)
✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000804141000804141 ETH (804141 gas * avg 1.000000001 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: /home/lu-xiangyu/foundry_test_project/broadcast/DeploySimpleStorage.s.sol/31337/run-latest.json
Sensitive values saved to: /home/lu-xiangyu/foundry_test_project/cache/DeploySimpleStorage.s.sol/31337/run-latest.json
== Return ==
0: address 0x5FbDB2315678afecb367f032d93F642f64180aa3
== Logs ==
SimpleStorage contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
== Return == : 这部分显示了部署脚本 run() 函数的返回值。0: address ... 表示函数返回了 1 个值(索引为 0),类型是 address,地址是 0x5Fb...aa3。这通常就是你部署的合约地址。
== Logs == : 这显示了脚本中所有 console.log() 语句的输出。这里输出的是合约的地址
## Setting up 1 EVM.
==========================
Chain 31337
Estimated gas price: 2.000000001 gwei
Estimated total gas used for script: 1045383
Estimated amount required: 0.002090766001045383 ETH
==========================
Setting up 1 EVM.: 表示 Foundry 正在为链上执行准备环境。
Chain 31337: 这是目标区块链的链 ID (Chain ID)。31337 是 Anvil 和 Hardhat 等本地开发网络的标准默认 ID。这再次确认你没有意外地将交易发送到主网或公共测试网。
Gas 估算: 在发送交易前,Foundry 会估算费用。
Estimated gas price: 估算的网络 Gas 价格。
Estimated total gas used: 估算执行你的脚本(部署合约)需要消耗的总 Gas 量。
Estimated amount required: (Gas Price * Gas Used) 的结果,即完成这次部署预估需要花费的 ETH。这是一个非常有用的功能,可以防止因资金不足导致交易失败。
##### anvil-hardhat
✅ [Success] Hash: 0x677e4d8c093176d255733c42b1392449431b95c5b56a7f40f5b015b2a4c6c4c5
Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Block: 1
Paid: 0.000804141000804141 ETH (804141 gas * 1.000000001 gwei)
✅ Sequence #1 on anvil-hardhat | Total Paid: 0.000804141000804141 ETH (804141 gas * avg 1.000000001 gwei)
anvil-hardhat: 这是一个标签,表示以下是针对名为 anvil-hardhat 的 RPC 端点(在这里是你的本地节点)的执行结果。
✅ [Success] Hash: ...: 这是最重要的部分。它确认你的交易已经成功被打包到区块链中。
Hash: 你的部署交易的唯一哈希值。
Contract Address: 链上实际创建的合约地址。
Block: 包含此交易的区块号(本地链刚启动,所以是第 1 个块)。
Paid: 实际支付的费用。注意,实际费用 (0.0008... ETH) 通常会比估算费用 (0.002... ETH) 低,因为估算会留出一些余量。
✅ Sequence #1 ...: 如果你的脚本包含多个交易,这里会有一个总的费用摘要。因为只有一个部署交易,所以总费用和单笔费用相同。
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: /home/lu-xiangyu/foundry_test_project/broadcast/DeploySimpleStorage.s.sol/31337/run-latest.json
Sensitive values saved to: /home/lu-xiangyu/foundry_test_project/cache/DeploySimpleStorage.s.sol/31337/run-latest.json
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.: 一个清晰的最终成功信息。
Transactions saved to: .../broadcast/...: Foundry 会将部署的公开记录(如交易哈希、合约地址等)保存到一个 JSON 文件中。这个 broadcast 目录是可以安全提交到 Git 的。它让 Foundry 能够跟踪已部署的合约,并支持 --resume 功能来继续一个失败的部署。
Sensitive values saved to: .../cache/...: 如果你的脚本中包含了敏感信息(例如硬编码的私钥,这是不推荐的做法),它会被保存在 cache 目录下的文件中。这个目录绝对不能提交到 Git,并且默认已经被包含在 .gitignore 里了。
cast工具包
cast 是 Foundry 工具套件中的一个核心组件,它的主要作用是让你能够通过终端(命令行界面)直接执行各种区块链操作,而无需编写脚本或打开图形界面。
cast 可以轻松获取链上的通用信息,而不仅仅是合约数据。
cast balance <地址>: 获取一个地址的原生代币余额(例如 ETH)。cast block <区块号|latest>:获取一个区块的详细信息cast receipt <交易哈希>: 获取一笔交易的回执,可以查看交易是否成功、消耗了多少 Gas、产生了哪些事件日志等。cast code <地址>: 获取一个地址部署的合约字节码。cast storage <合约地址> <存储槽位>: 读取合约中特定存储槽的原始数据,是高级调试的利器。
cast 提供了大量进行数据编码、解码和哈希计算的工具,这在开发和调试时非常有用。
cast keccak "<数据>": 计算一个字符串的 Keccak-256 哈希值。cast sig "<函数签名>": 计算函数签名的 4 字节选择器(selector)。例如 cast sig "transfer(address,uint256)" 会返回 0xa9059cbb。cast --to-wei <数量> [单位]": 将 ETH 单位(如 ether, gwei)转换成 wei。cast abi-encode "<签名>" [参数...]: 将函数调用和参数进行 ABI 编码,生成原始的 calldata。cast 4byte <函数选择器>: 通过 4 字节的函数选择器反向查询可能的函数签名。
cast 还能处理本地的钱包操作,无需连接网络。
- cast wallet new: 生成一个新的随机钱包(地址和私钥)。
cast wallet sign "<消息>": 使用私钥对一条消息进行签名。- cast wallet verify ...: 验证一个签名是否有效。
cast还可以通过call和send方法来和已经部署的合约函数进行交互
cast call $CONTRACT_ADDRESS "number()" --rpc-url http://127.0.0.1:8545
#查询合约主人的数字
0x000000000000000000000000000000000000000000000000000000000000002a
cast send $CONTRACT_ADDRESS "setMessage(string)" "Hacked Message" --private-key $USER_PRIVATE_KEY --rpc-url http://127.0.0.1:8545
#USER_PRIVATE_KEY是虚拟链中的第二个私钥
#因为我们设置了权限,所以只有合约的主人可以设置Message
Error: Failed to estimate gas: server returned an error response: error code 3: execution reverted: custom error 0x118cdaa7: 00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8, data: "0x118cdaa700000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8": OwnableUnauthorizedAccount(0x70997970C51812dc3A010C7d01b50e0d17dc79C8)
cast send $CONTRACT_ADDRESS "setMessage(string)" "Message from Owner" --private-key $PRIVATE_KEY --rpc-url http://127.0.0.1:8545
blockHash 0x9cd071cfa8ff6d3114ecfa5d93c1de3ea78f6f5ea85743fb15eb7a29bac8b8a8
blockNumber 3
contractAddress
cumulativeGasUsed 32410
effectiveGasPrice 771695582
from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
gasUsed 32410
logs [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xfe805c34be9e63a416b9aba99c6c62782dafb9620895945675bbbe5131aeb1a1","0x04269b400dd3820adab59de1c6535844e8b15ce9ace2ce357e5240c8083489ac","0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"],"data":"0x","blockHash":"0x9cd071cfa8ff6d3114ecfa5d93c1de3ea78f6f5ea85743fb15eb7a29bac8b8a8","blockNumber":"0x3","blockTimestamp":"0x68858159","transactionHash":"0x1e6e7d9ab806714d5315c0018def77031fc9a928c7988e923e2f9b3c6e033edd","transactionIndex":"0x0","logIndex":"0x0","removed":false}]
logsBloom 0x00000000000000000000000000000000000000000000000000008000000200000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001000000000000000000000000000040000000200000000000000000000000002000000000000000000000000000000000000000000040000001000000000000000000000000000000000000
root
status 1 (success)
transactionHash 0x1e6e7d9ab806714d5315c0018def77031fc9a928c7988e923e2f9b3c6e033edd
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
to 0x5FbDB2315678afecb367f032d93F642f64180aa3
Reference
[[foundry doc]]