Published on

foundry项目基本构建

Authors

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]]