🖐🏻 免责声明
本教程仅供学习交流使用,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,请各读者自觉遵守相关法律法规。
# 官网
https://github.com/foundry-rs/foundry
# 教程
https://book.getfoundry.sh/getting-started/installation
# 安装(Windows)
- 直接下载预编译版本:https://github.com/foundry-rs/foundry/releases

- 解压后,四大组件均在其中,只要配上环境变量就可以丝滑使用了。

- 测试安装是否成功
forge --version
# forge使用
# forge查看帮助
# 新建项目
forge init hello hello为项目名
# 编译项目
forge build
# 目录结构

# 单元测试
forge test,测试前会自动重新编译代码
运行test目录下单元测试名称为CounterTest,测试用例为testIncrement
forge test --match-contract CounterTest --match-test testIncrement
- 运行test目录下单元测试文件名称为Counter.t.sol,测试用例为testIncrement
forge test --match-path test/Counter.t.sol,--match-test testIncrement
- 当合约内容有变动时,就会重新运行所有的单元测试
forge test --watch
# 依赖管理
# 安装依赖包
forge install transmissions11/solmate
报错:the target directory is a part of or on its own an already initialized git rspository
解决:把未暂存的文件暂存并提交即可
# 删除依赖包
forge remove lib/solmate
这种只会删除lib目录下的文件,配置文件(.gitmodules)里还在
# 更新依赖包
forge update lib/solmate
如何让VSCode能识别到依赖包里的合约 首先,在工程目录下创建remappings.txt文件 然后,运行以下命令:
forge remappings
在控制台下,产生类似下面结果,然后将其拷贝到remappings.txt文件中
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
*注意一,在安装过程中可能出现以下错误信息
fatal: unable to access 'https://github.com/foundry-rs/forge-std/': OpenSSL SSL_read: Connection was reset, errno 10054
解决方法,修改git 配置信息:
git config --global http.sslVerify "false"
*注意二,如果出现连接超时,通常是PC上安装了翻墙软件。
解决办法,修改git 配置信息:
git config --global http.proxy 127.0.0.1:7890
# 测试代码写法
# 引用测试类
import "forge-std/Test.sol";
# 引用目标类
import "../src/Counter.sol";
# 继承测试类
contract CounterTest is Test
# 部署合约
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
# 断言测试
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
# 日志打印
import "forge-std/Test.sol"; //这里面含console的引用了
console.log("111111")
forge test -vvv
加上-vvv就可以在控制台看到打印了
# Cheatcodes 作弊码
- 为了达到单元测试预期效果,需要引入作弊码,比如修改时间、账号或区块等...
# 更改地址
vm.prank(address(10));
# 测试Event事件
- 首先,测试合约里调用被测试合约的方法提交(emit)事件
- 然后,在单元测试合约,再次提交(emit)同样的事件
- 最后,使用vm.expectEmit()断言比较两次提交(emit)事件的参数是否一致,vm.expectEmit()必须放在测试代码前面
# 代码
- TransferLog.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
contract TransferLog {
event Transfer(address indexed from, address indexed to, uint256 amount);
function transfer(address _to, uint256 _amount) public {
//logic here...
emit Transfer(msg.sender, _to, _amount);
}
}
- TransferLogTest.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/TransferLog.sol";
contract TransferLogTest is Test {
event Transfer(address indexed from, address indexed to, uint256 amount);
TransferLog public transferLog;
address public to = address(100);
uint256 public amount = 1000;
function setUp() public {
transferLog = new TransferLog();
}
function testWithExpectEmit() public {
vm.expectEmit(true, true, false, true);
transferLog.transfer(to, amount);
emit Transfer(address(this), to, amount);
}
function testFailWithExpectEmitAmount() public {
vm.expectEmit(true, true, false, true);
transferLog.transfer(to, amount);
emit Transfer(address(this), to, amount + 1);
}
function testWithExpectEmitAmount() public {
vm.expectEmit(true, true, false, false);
transferLog.transfer(to, amount);
emit Transfer(address(this), to, amount + 1);
}
}
上面emit事件Transfer有三个参数,前两个indexed参数,最后一个amount
expectEmit 的前三个bool参数,指的是是否校验indexed参数是否一致,像这里第三个indexed参数都不存在,就直接写为false,最后一个bool参数是校验普通参数amount是否一致。
# Trace解析
- forge test -vvv 为测试用例失败时,输出Trace信息
- forge test -vvvv 输出Trace信息
# Fork网络
fork 指定的区块
forge test --match-contract CounterSingleTest --fork-url https://eth-mainnet.g.alchemy.com/v2/Gu8Au3-L5xNG32c-NHmB5eKNBxJZUayA --fork-block-number 16874791
# Assert
- AssertTrue 断言 True (真)
- AssertEq 断言 Equal (等于)
- AssertGt 断言 Greater than(大于)
- AssertGe 断言 Greater than or equal to(大于等于)
- AssertLt 断言 Less than(小于)
- AssertLe 断言 Less than or equal to(小于等于)
# 部署合约
# 本地
# 启动节点
anvil# 部署命令
forge create --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/MyToken.sol:MyToken
# 测试网络
forge create --rpc-url https://eth-goerli.g.alchemy.com/v2/X0MHoR7c5ni-Lq0d86Q_s9B_6dNfqshC --constructor-args 1000000 --private-key b6e0878b81793a40ac4e6a5c63022dbcc7a39b248a13078046d1da21a83f24e1 --etherscan-api-key 2HP144343P432W6A8X1IAT3UTJ7R2DYNBA --verify src/MyToken.sol:MyToken517
# Cast命令
# Chain
cast -V
cast -h
# cast balance
cast balance --rpc-url https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83 0x0000000000000000000000000000000000000000
# cast chain-id
cast chain-id --rpc-url https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83
cast chain-id --rpc-url https://eth-goerli.g.alchemy.com/v2/X0MHoR7c5ni-Lq0d86Q_s9B_6dNfqshC
cast chain-id --rpc-url https://arb-goerli.g.alchemy.com/v2/JRCJrbrGuqCl0FwWrFvn3HSqXZouJEbL
# cast chain
cast chain --rpc-url https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83
cast chain --rpc-url https://eth-goerli.g.alchemy.com/v2/X0MHoR7c5ni-Lq0d86Q_s9B_6dNfqshC
# cast client
cast client --rpc-url https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83
cast client --rpc-url https://eth-goerli.g.alchemy.com/v2/X0MHoR7c5ni-Lq0d86Q_s9B_6dNfqshC
cast client --rpc-url https://arb-goerli.g.alchemy.com/v2/JRCJrbrGuqCl0FwWrFvn3HSqXZouJEbL
# Block
cast -V
#獲取區塊,默認獲取最後一個區塊
export mainnet=https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83
cast block --rpc-url $mainnet --json --field number
cast block --rpc-url $mainnet pending
cast block --rpc-url $mainnet --full
#根據時間獲取區塊
cast find-block --rpc-url $mainnet 1684485827
#獲取最後區塊編號
cast block-number --rpc-url $mainnet
#獲取當前gas的價格
cast gas-price --rpc-url $mainnet
#獲取basefee基礎費
cast base-fee --rpc-url $mainnet
cast base-fee --rpc-url $mainne 17292427
#獲取某個區塊出塊時間
cast age --rpc-url $mainnet
cast age --rpc-url $mainnet 1
# Abi
cast -V
export mainnet=https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83
#對調用參數encode & decode
cast abi-encode "transfer(address, uint256)" 0x5d0dd9bd309bf751655403fd7418d29939b6220c 100
cast --abi-decode "transfer()(address, uint256)" 0x0000000000000000000000005d0dd9bd309bf751655403fd7418d29939b6220c0000000000000000000000000000000000000000000000000000000000000064
#struct結構傳參encode
cast abi-encode "transfer((address, uint256))" "(0x5d0dd9bd309bf751655403fd7418d29939b6220c,100)"
#對調用方法和參數encode
cast calldata "transfer(address, uint256)" 0x5d0dd9bd309bf751655403fd7418d29939b6220c 100
cast --calldata-decode "transfer(address, uint256)" 0xa9059cbb0000000000000000000000005d0dd9bd309bf751655403fd7418d29939b6220c0000000000000000000000000000000000000000000000000000000000000064
cast pretty-calldata 0xa9059cbb0000000000000000000000005d0dd9bd309bf751655403fd7418d29939b6220c0000000000000000000000000000000000000000000000000000000000000064
cast 4byte-decode 0xa9059cbb0000000000000000000000005d0dd9bd309bf751655403fd7418d29939b6220c0000000000000000000000000000000000000000000000000000000000000064
#對調用方法解碼
cast 4byte 0xa9059cbb
#對event事件decode
cast 4byte-event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
#將字符串轉換成字節bytes32
cast --format-bytes32-string "hello world"
#將字節轉換成字符串string
cast --parse-bytes32-string "0x68656c6c6f20776f726c64000000000000000000000000000000000000000000"
#將字符串轉換utf8
cast --from-utf8 "hello world"
#將ascii 轉換成字符串
cast --to-ascii "0x68656c6c6f20776f726c64"
#將整數轉換成32個字節
cast --to-uint256 1
# Account
cast -V
export mainnet=https://mainnet.infura.io/v3/8c5975bf0119494789ae0c227dce2b83
#獲取餘額
cast balance --rpc-url $mainnet beer.eth
cast balance --rpc-url $mainnet --ether 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
#獲取nonce 值
cast nonce --rpc-url $mainnet 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
#獲取ENS
cast lookup-address --rpc-url $mainnet 0x941F211a032eF4680b6b906aA651bf39B0a87B66
#獲取ENS 應對地址
cast resolve-name --rpc-url $mainnet vitalik.eth
#獲取插槽slot數據
cast storage --rpc-url $mainnet 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 0
#獲取合約的bytescode
cast code --rpc-url $mainnet 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
#獲取合約源代碼
cast etherscan-source 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
cast etherscan-source -d weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
# Transaction
cast -V
export privateKey=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# 转账
cast send --private-key $privateKey 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --value 10ether
# 获取账号余额
cast balance 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --ether
# 创建合约
forge create --private-key $privateKey src/Counter.sol:Counter
# 调用合约方法
cast send --private-key $privateKey 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 "setNumber(uint256)" 100
# 调用static 合约方法
cast call 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 "number()(uint256)"
# 获取Transation 信息
cast tx 0x914610aa6c9bb7659a9b5b8ae1c0575b4dfce51226a01ecd1180d7553977d0e1
# Wallet
cast -V
# 创建钱包方式一
cast wallet new
# 创建钱包方式二
mkidr keystore
cast wallet new keystore
#根据json钱包获取地址
cast wallet address --keystore cb.json
#签名
export privateKey=0x5437dd374622a1d0f73b712861a38d01ebd93e7167cc8f94f728d7530ac3d67a
cast wallet sign --private-key $privateKey "hello"
#验签
cast wallet verify --address 0xb6e140994bC080ebE7eC570381940EC4e23bC369 "hello" 0x599155b8883fde4400530bc0b553652dcc87cacaf7ae322d9b706bb58c45dc84280e84e5c122567d967fc25e2a1dbce8ee57b1d49f1ead81bf8630758d62ad131b
#生成靓号
cast wallet vanity --starts-with 00 --ends-with 00

