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;
}
}
测试函数必须具有
external
或public
可见性
对于合约
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
却能通过测试。这是因为OwnerUpOnlyTest
是OwnerUpOnly
的所有者
为了确保绝对不是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 中 始终成立
- ERC20 代币的总量总是等于用户余额的总和
assertGe(
token.totalAssets(),
token.totalSupply()
)
assertEq(
token.totalSupply(),
sumBalanceOf
)
assertEq(
pool.outstandingInterest(),
test.naiveInterest()
)