Foundry-合约编写及测试框架

🖐🏻 免责声明

本教程仅供学习交流使用,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,请各读者自觉遵守相关法律法规。

# 官网

https://github.com/foundry-rs/foundry

# 教程

https://book.getfoundry.sh/getting-started/installation

# 安装(Windows)

  1. 直接下载预编译版本:https://github.com/foundry-rs/foundry/releases

1718593590403

  1. 解压后,四大组件均在其中,只要配上环境变量就可以丝滑使用了。1718593643395
  2. 测试安装是否成功

forge --version1718593769853

# forge使用

# forge查看帮助1718593910363

# 新建项目

forge init hello hello为项目名1718595328425

# 编译项目

forge build

# 目录结构

1718603348847

# 单元测试

  • forge test,测试前会自动重新编译代码1718603544956

  • 运行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 rspository1718683020199

解决:把未暂存的文件暂存并提交即可

# 删除依赖包

forge remove lib/solmate

这种只会删除lib目录下的文件,配置文件(.gitmodules)里还在1718605220792

# 更新依赖包

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

# ☕ 请我喝咖啡

如果本文章对您有所帮助,不妨请作者我喝杯咖啡 :)

pay


# ☀️ 广告时间

现承接以下业务,欢迎大家支持:)

  • Web 2.0 & Web 3.0应用定制
  • Web 3.0专项脚本定制与优化
  • 数据爬虫需求快速响应
  • 网站/公众号/小程序一站式开发
  • 毕业设计与科研项目支持
  • 企业管理软件定制:ERP, MES, CRM, 进销存系统等

联系方式:

X:@motuoka

V:ck742931485

wx