Appearance
Foundry 学习笔记
Simple Storage
1、forge init 用来初始化一个新的 Foundry 项目
2、forge compile 或 forge 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 \
--broadcastvm.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: 7770x5FC8d32690cc91D4c39d9d3abcBD16989F875707是合约地址- 注意,写合约是
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-zksync 和 foundryup 切换 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.sender、fundMe.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 种测试:
- Unit: Testing a single function
- Integration: Testing multiple functions
- Forked: Testing on a forked network
- 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 消耗