跳到主要内容

Foundry

rust 环境下的 Solidity IDE官网教程

相关命令

# 安装工具链
curl -L https://foundry.paradigm.xyz | bash

# 安装 foundry
foundryup --branch master

# 创建项目
forge init <project name>

# 编译
forge build

# 测试
forge test

# 克隆已验证的合约
forge clone 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 WETH9

# 安装依赖, 依赖会被放在 lib 目录下
forge install transmissions11/solmate

# 查看依赖的导入路径和实际路径的映射
forge remappings

# 更新依赖
forge update lib/solmate

# 删除依赖
forge remove lib/solmate

测试

使用命令 forge test 运行测试代码。

指定测试合约名和测试函数,测试函数名可以是全名,也可以是简写

forge test --match-contract ComplicatedContractTest --match-test test_Deposit

测试合约需要继承 Test.sol

pragma solidity 0.8.10;

import "forge-std/Test.sol";

// 继承自 Test 合约
contract ContractBTest is Test {

uint256 testNumber;
// 在每个测试用例运行之前调用的可选函数, 例如创建合约,初始化变量
function setUp() public {
testNumber = 42;
}

// 以 test 开头的测试函数
function test_NumberIs42() public {
assertEq(testNumber, 42);
}

// 以 testFail 开头的测试函数, 期望结果是失败的测试函数
function testFail_Subtract43() public {
testNumber -= 43;
}
}

测试函数必须具有externalpublic可见性

对于合约

pragma solidity 0.8.10;

import "forge-std/Test.sol";

error Unauthorized();

contract OwnerUpOnly {
address public immutable owner;
uint256 public count;

constructor() {
owner = msg.sender;
}

function increment() external {
// 要求调用者是合约的 owner
if (msg.sender != owner) {
revert Unauthorized();
}
count++;
}
}

contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;

function setUp() public {
upOnly = new OwnerUpOnly();
}

function test_IncrementAsOwner() public {
assertEq(upOnly.count(), 0);
upOnly.increment();
assertEq(upOnly.count(), 1);
}
}

increment 方法要求调用者必须是合约的 owner, 但运行 forge test 却能通过测试。这是因为OwnerUpOnlyTestOwnerUpOnly的所有者

为了确保绝对不是owner 无法调用,使用 prank

contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;

// ...

// 期望测试失败的情况。由于修改了调用者地址为零地址。
function testFail_IncrementAsNotOwner() public {
vm.prank(address(0));
upOnly.increment();
}
}

测试通过。

跟踪测试失败的情况

forge test -vvvv --match-test testFail_IncrementAsNotOwner

关于 -vvvv ,用于跟踪调用栈, 有两种指令

  • -vvv 为失败的测试跟踪调用栈
  • -vvvv 为所有测试跟踪调用站

模糊测试

模糊测试:Fuzz Testing

pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract Safe {
receive() external payable {}

function withdraw() external {
payable(msg.sender).transfer(address(this).balance);
}
}

contract SafeTest is Test {
Safe safe;

// Needed so the test contract itself can receive ether
// when withdrawing
receive() external payable {}

function setUp() public {
safe = new Safe();
}

function test_Withdraw() public {
payable(address(safe)).transfer(1 ether);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + 1 ether, postBalance);
}
}

测试从 Safe 合约中提取以太币。test_Withdraw 函数提取的是固定的金额。因此至少需要一个参数用来测试提取任意金额的以太币。通过给测试函数添加参数的形式。forge test 运行时会执行多次该函数并自动带入不同的参数。只有每次都成功才算成功。

contract SafeTest is Test {
// ...

function testFuzz_Withdraw(uint256 amount) public {
payable(address(safe)).transfer(amount);
uint256 preBalance = address(this).balance;
safe.withdraw();
uint256 postBalance = address(this).balance;
assertEq(preBalance + amount, postBalance);
}
}

运行测试时,会报错。提取金额大于合约的可用金额,因为测试合约默认获得的以太币数量是 2**96 wei。自动带入的参数超过这个值会测试就会失败。

因此可以限制提取金额是 uint96 类型。以确保不会提取超过拥有的数量。

或者使用作弊码来排除某些情况。

function testFuzz_Withdraw(uint96 amount) public {
vm.assume(amount > 0.1 ether);
// snip
}

forge test 运行模糊测试时,会打印出如下的结果

[PASS] testFuzz_Withdraw(uint96) (runs: 256, μ: 22873, ~: 22873)
  • runs 模糊测试的次数, 可配置
  • μ 所有模糊测试运行中使用的平均 gas
  • ~ 所有模糊测试运行中使用的中位数 gas

不变测试

不变测试允许针对来自预定义合约的预定义函数调用的随机序列来测试一组不变表达式。执行每个函数调用后,所有定义的不变量都会被断言。例如

  • Uniswap 中 xy=kx * y = k 始终成立
  • ERC20 代币的总量总是等于用户余额的总和
assertGe(
token.totalAssets(),
token.totalSupply()
)

assertEq(
token.totalSupply(),
sumBalanceOf
)

assertEq(
pool.outstandingInterest(),
test.naiveInterest()
)

差异测试

部署和验证

# 部署合约
forge create --rpc-url <your_rpc_url> \
--constructor-args "ForgeUSD" "FUSD" 18 1000000000000000000000 \
--private-key <your_private_key> \
--etherscan-api-key <your_etherscan_api_key> \
--verify \
src/MyToken.sol:MyToken

# 验证合约
forge verify-contract \
--chain-id 11155111 \
--num-of-optimizations 1000000 \
--watch \
--constructor-args $(cast abi-encode "constructor(string,string,uint256,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000) \
--etherscan-api-key <your_etherscan_api_key> \
--compiler-version v0.8.10+commit.fc410830 \
<the_contract_address> \
src/MyToken.sol:MyToken

Gas 追踪

foundry.toml 加入

# 报告所有合约的 gas
gas_reports = ["*"]

# 报告指定合约的 gas
gas_reports = ["MyContract", "MyContractFactory"]

# 忽略合约
gas_reports_ignore = ["Example"]

生成 gas 报告: forge test --gas-report

为所有的测试函数生成 gas 快照

forge snapshot

可加的参数

  • --snap <FILE_NAME> 输出到文件
  • --asc 对结果进行升序排序
  • --desc 对结果进行降序排序

Cast

Cast 是 Foundry 用于执行以太坊 RPC 调用的命令行工具。您可以进行智能合约调用、发送交易或检索任何类型的链数据

例如

# 检索 DAI 代币的总供应量
cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf