Skip to content

Foundry 学习笔记

Simple Storage

1、forge init 用来初始化一个新的 Foundry 项目

2、forge compileforge build 用来编译智能合约,对应的 abi 会输出到 out/

3、anvil 可以启动一个本地的区块链服务

4、要知道怎么在 MetaMask 钱包里导入 anvil 网络和测试账号

5、Ethereum JSON-RPC Specification 这是以太坊 JSON-RPC 协议,如果你使用 Foundry 那么它们都是已经封装好的了,但是如果你使用 Python、Go 的话,就需要使用 HTTPS 来访问了

6、使用 Foundry 部署合约有两种方式,命令行和 Solidity 脚本

命令行:

bash
forge create SimpleStorage --rpc-url $RPC_URL \
    --interactive \
    --broadcast
  • --interactive 通过交互式命令提供私钥

Solidity 脚本:

solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {Script} from "forge-std/Script.sol";
import {SimpleStorage} from "../src/SimpleStorage.sol";

contract DeploySimpleStorage is Script {
    function run() external returns (SimpleStorage) {
        vm.startBroadcast();
        SimpleStorage simpleStorage = new SimpleStorage();
        vm.stopBroadcast();

        return simpleStorage;
    }
}
bash
forge script script/DeploySimpleStorage.s.sol --rpc-url $RPC_URL \
    --private-key $PRIVATE_KEY \
    --broadcast
  • vm.startBroadcast()vm.stopBroadcast() 是 Foundry 在告诉你哪些操作要真的发到链上,也就是说会用私钥签名、消耗 gas、链上能查到合约地址等
  • 如果不指定网络和私钥,直接使用 forge script script/DeploySimpleStorage.s.sol 部署合约的话,也可以成功,Foundry 会将合约部署到一个临时启动的 Anvil 上,程序退出之后就啥都没了
  • function run() external returns (SimpleStorage) { 这个函数签名容易写错,要注意
  • vm.startBroadcast()--broadcast 的区别:vm.startBroadcast() 标记哪些操作是交易;--broadcast 允许这些交易真的发出去

7、Foundry 的 cast 工具,一个常见用途就是将 Hex 转为十进制数字,例如 cast --to-base 0x714c2 dec 的输出是 464066

8、使用 Foundry 调用合约也有两种方式,命令行和 Solidity 脚本

命令行:

bash
# 写合约
cast send 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 \
    "store(uint256)" 777 \
    --rpc-url $RPC_URL \
    --private-key $PRIVATE_KEY

# 读合约
cast call 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 \
    "retrieve()"
# Output: 0x0000000000000000000000000000000000000000000000000000000000000309

cast --to-base 0x0000000000000000000000000000000000000000000000000000000000000309 \
    dec
# Output: 777
  • 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 是合约地址
  • 注意,写合约是 cast send,读合约是 cast call

9、将合约部署到 Sepolia 测试网

Anchemy 上创建一个私有的区块链节点,网络选择 Sepolia 测试网,创建成功之后将 https://eth-sepolia.g.alchemy.com/v2/XXX 拷贝到 RPC_URL 环境变量

然后到 MetaMask 上获取 Sepolia 测试网的一个账号的私钥,拷贝到 PRIVATE_KEY 环境变量

最后重新执行部署命令或 Solidity 脚本即可,成功部署到测试网可以在 etherscan.io 上查看合约,例如 这个合约

10、安装完 Foundry ZKsync 之后,可以使用 foundryup-zksyncfoundryup 切换 forge 版本

Fund Me

1、下载 chainlink 依赖并使用

使用 forge install smartcontractkit/chainlink-brownie-contracts@0.6.1 下载之后,打开 foundry.toml 添加路径映射

toml
remappings = [
    "@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/",
]

然后在 Solidity 文件这样使用:

solidity
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

2、forge test -vv 执行测试用例。v 越多,Forge 给你吐的内部细节越多

3、理解 msg.senderfundMe.getOwner()address(this) 的区别

solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";

contract FundMeTest is Test {
    FundMe fundMe;

    function setUp() external {
        fundMe = new FundMe(
            address(0x694AA1769357215DE4FAC081bf1f309aDC325306)
        );
    }

    function testOwnerIsMsgSender() public view {
        console.log(msg.sender);
        console.log(fundMe.getOwner());
        console.log(address(this));
        assertEq(fundMe.getOwner(), address(this));
    }
}
  • msg.sender 外部测试 EOA(EOA:被私钥控制的账户)
  • address(this) 测试合约
  • fundMe.getOwner() 测试合约

4、使用 forge test --match-test testMinimumDollarIsFive 可以运行指定的测试用例

5、--fork-url 把真实链搬到你电脑上(本地 EVM anvil 里的快照)跑测试。例如:forge test --match-test testPriceFeedVersionIsAccurate -vvv --fork-url $RPC_URL

solidity
function testPriceFeedVersionIsAccurate() public view {
    assertEq(fundMe.getVersion(), 4);
}

6、4 种测试:

  1. Unit: Testing a single function
  2. Integration: Testing multiple functions
  3. Forked: Testing on a forked network
  4. Staging: Testing on a live network (testnet or mainnet)

7、理解覆盖测试的输出 forge coverage --fork-url $RPC_URL。主要是 Lines 和 Statements 的区别,Lines 是行覆盖率,而 Statements 是语句覆盖率,它比 lines 更细一点,比如:if (x > 0) a++; else b++; 这里面有 3 条语句

bash
Ran 3 tests for test/FundMeTest.t.sol:FundMeTest
[PASS] testMinimumDollarIsFive() (gas: 5826)
[PASS] testOwnerIsMsgSender() (gas: 5897)
[PASS] testPriceFeedVersionIsAccurate() (gas: 16671)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.37s (447.74ms CPU time)

Ran 1 test suite in 2.10s (1.37s CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)

╭---------------------------+---------------+---------------+---------------+---------------╮
| File                      | % Lines       | % Statements  | % Branches    | % Funcs       |
+===========================================================================================+
| script/DeployFundMe.s.sol | 0.00% (0/4)   | 0.00% (0/3)   | 100.00% (0/0) | 0.00% (0/1)   |
|---------------------------+---------------+---------------+---------------+---------------|
| src/FundMe.sol            | 18.42% (7/38) | 15.62% (5/32) | 0.00% (0/7)   | 30.00% (3/10) |
|---------------------------+---------------+---------------+---------------+---------------|
| src/PriceConverter.sol    | 0.00% (0/7)   | 0.00% (0/8)   | 100.00% (0/0) | 0.00% (0/2)   |
|---------------------------+---------------+---------------+---------------+---------------|
| Total                     | 14.29% (7/49) | 11.63% (5/43) | 0.00% (0/7)   | 23.08% (3/13) |
╰---------------------------+---------------+---------------+---------------+---------------╯

8、编写第一个 Mock,即 PriceFeed Mock

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import {Script} from "forge-std/Script.sol";
import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol";

contract HelperConfig is Script {
    uint8 public constant DECIMALS = 8; // 返回的价格,用 8 位小数
    int256 public constant INITIAL_PRICE = 2000e8; // ETH = 2000 美元(按 8 位小数存)

    struct NetworkConfig {
        address priceFeed; // ETH/USD price feed address
    }

    NetworkConfig public activateNetworkConfig;

    constructor() {
        if (block.chainid == 11155111) {
            activateNetworkConfig = getSepoliaEthConfig();
        } else {
            activateNetworkConfig = getOrCreateAnvilEthConfig();
        }
    }

    function getSepoliaEthConfig() public pure returns (NetworkConfig memory) {
        NetworkConfig memory sepoliaConfig = NetworkConfig({
            priceFeed: 0x694AA1769357215DE4FAC081bf1f309aDC325306
        });
        return sepoliaConfig;
    }

    function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) {
        if (activateNetworkConfig.priceFeed != address(0)) {
            return activateNetworkConfig;
        }

        vm.startBroadcast();
        MockV3Aggregator mockPriceFeed = new MockV3Aggregator(DECIMALS, INITIAL_PRICE);
        vm.stopBroadcast();

        NetworkConfig memory anvilConfig = NetworkConfig({
            priceFeed: address(mockPriceFeed)
        });
        return anvilConfig;
    }
}
  • https://chainlist.org/ 这里可以常看主流区块链的 block.chainid
  • MockV3Aggregator mockPriceFeed = new MockV3Aggregator(DECIMALS, INITIAL_PRICE); 理解 MockV3Aggregator 的入参

9、Foundry 的 cheatcodes 常见的几个:

  • vm.startBroadcast/vm.stopBroadcast
  • vm.expectRevert
  • vm.prank 指定下一个事务由哪个地址发起
  • makeAddr
  • vm.deal 为某个地址设余额
  • hoax
  • txGasPrice

10、一个方便调试 Solidity 代码的工具 chisel,类似 python 解释器逐行解释执行

11、https://www.evm.codes/ 这个网站记录了合约的每种 opcodes 的 gas 消耗