智能合约测试:使用Foundry进行形式化验证
大家好,我是欧阳瑞(Rich Own)。今天想和大家聊聊智能合约测试这个重要话题。作为一个Web3探索者,我深知智能合约测试的重要性。一个小小的漏洞可能会导致数百万美元的损失。今天就来分享一下如何使用Foundry框架进行智能合约测试和形式化验证。
为什么智能合约测试如此重要?
智能合约一旦部署到区块链上就无法修改,因此在部署前必须进行充分的测试。根据统计,超过80%的智能合约漏洞可以通过充分的测试发现。
什么是Foundry?
Foundry是一个用于以太坊智能合约开发、测试和部署的工具链。它由Rust编写,提供了:
- 快速的测试执行
- 强大的形式化验证工具
- 内置的模糊测试
- 集成的调试器
安装Foundry
bash
# 安装Foundry
curl -L https://foundry.paradigm.xyz | bash
# 初始化Foundry
foundryup
# 验证安装
forge --version
cast --version
anvil --version
创建Foundry项目
bash
# 创建新项目
forge init my-foundry-project
cd my-foundry-project
# 查看项目结构
ls -la
项目结构:
my-foundry-project/
├── lib/ # 依赖库
├── src/ # 合约源码
│ └── Contract.sol
├── test/ # 测试文件
│ └── Contract.t.sol
├── foundry.toml # Foundry配置
├── remappings.txt # 依赖重映射
└── README.md
编写智能合约
solidity
// src/Vault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Vault {
mapping(address => uint256) public balances;
event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount);
function deposit() external payable {
require(msg.value > 0, "Deposit amount must be positive");
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
require(amount > 0, "Withdrawal amount must be positive");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawal(msg.sender, amount);
}
function getBalance(address user) external view returns (uint256) {
return balances[user];
}
}
编写单元测试
solidity
// test/Vault.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultTest is Test {
Vault public vault;
address public user = address(0x1234);
function setUp() public {
vault = new Vault();
vm.deal(user, 100 ether);
}
function testDeposit() public {
vm.prank(user);
vault.deposit{value: 10 ether}();
assertEq(vault.getBalance(user), 10 ether);
}
function testWithdraw() public {
vm.prank(user);
vault.deposit{value: 10 ether}();
vm.prank(user);
vault.withdraw(5 ether);
assertEq(vault.getBalance(user), 5 ether);
}
function testWithdrawInsufficientBalance() public {
vm.prank(user);
vault.deposit{value: 5 ether}();
vm.expectRevert("Insufficient balance");
vm.prank(user);
vault.withdraw(10 ether);
}
function testDepositZeroAmount() public {
vm.expectRevert("Deposit amount must be positive");
vm.prank(user);
vault.deposit{value: 0}();
}
}
运行测试
bash
# 运行所有测试
forge test
# 运行特定测试
forge test --match-test testDeposit
# 显示详细输出
forge test -vvv
# 生成测试报告
forge test --coverage
形式化验证
什么是形式化验证?
形式化验证是一种数学方法,用于证明程序的正确性。它可以确保合约满足特定的属性。
使用Foundry进行形式化验证
solidity
// test/VaultFormal.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultFormal is Test {
Vault public vault;
address public constant USER = address(0x1234);
function setUp() public {
vault = new Vault();
vm.deal(USER, type(uint256).max);
}
function invariant_balanceNeverNegative() public view {
assert(vault.getBalance(USER) >= 0);
}
function invariant_totalBalanceEqualsDeposits() public {
uint256 initialBalance = address(vault).balance;
vm.prank(USER);
uint256 depositAmount = 100 ether;
vault.deposit{value: depositAmount}();
assertEq(address(vault).balance, initialBalance + depositAmount);
}
function testFuzzDepositWithdraw(uint256 amount) public {
vm.assume(amount > 0);
vm.assume(amount <= 1000 ether);
vm.prank(USER);
vault.deposit{value: amount}();
uint256 balance = vault.getBalance(USER);
assertEq(balance, amount);
vm.prank(USER);
vault.withdraw(amount);
assertEq(vault.getBalance(USER), 0);
}
}
模糊测试
solidity
// test/VaultFuzz.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultFuzzTest is Test {
Vault public vault;
function setUp() public {
vault = new Vault();
}
function testFuzzDeposit(uint256 amount) public {
vm.assume(amount > 0);
vm.assume(amount <= 100 ether);
address user = address(uint160(uint256(keccak256(abi.encodePacked(amount)))));
vm.deal(user, amount);
vm.prank(user);
vault.deposit{value: amount}();
assertEq(vault.getBalance(user), amount);
assertEq(address(vault).balance, amount);
}
function testFuzzWithdraw(uint256 depositAmount, uint256 withdrawAmount) public {
vm.assume(depositAmount > 0);
vm.assume(withdrawAmount > 0);
vm.assume(depositAmount <= 100 ether);
vm.assume(withdrawAmount <= depositAmount);
address user = address(0x5678);
vm.deal(user, depositAmount);
vm.prank(user);
vault.deposit{value: depositAmount}();
vm.prank(user);
vault.withdraw(withdrawAmount);
assertEq(vault.getBalance(user), depositAmount - withdrawAmount);
}
}
符号执行
bash
# 使用符号执行
forge inspect Vault methods
# 生成ABI
forge inspect Vault abi
# 生成接口
forge inspect Vault interface
集成测试
solidity
// test/VaultIntegration.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultIntegrationTest is Test {
Vault public vault;
address public alice = address(0x1111);
address public bob = address(0x2222);
function setUp() public {
vault = new Vault();
vm.deal(alice, 100 ether);
vm.deal(bob, 100 ether);
}
function testMultiUserInteraction() public {
vm.prank(alice);
vault.deposit{value: 50 ether}();
vm.prank(bob);
vault.deposit{value: 30 ether}();
assertEq(vault.getBalance(alice), 50 ether);
assertEq(vault.getBalance(bob), 30 ether);
assertEq(address(vault).balance, 80 ether);
vm.prank(alice);
vault.withdraw(20 ether);
assertEq(vault.getBalance(alice), 30 ether);
assertEq(address(vault).balance, 60 ether);
}
}
测试策略最佳实践
1. 测试覆盖率
bash
# 生成覆盖率报告
forge coverage --report lcov
# 查看覆盖率
genhtml lcov.info --output-directory coverage-report
2. 测试分层
单元测试 → 集成测试 → 端到端测试 → 形式化验证
3. 属性测试
solidity
function invariant_alwaysTrue() public view {
// 这个不变式应该永远为真
assert(true);
}
使用Anvil进行本地测试
bash
# 启动本地节点
anvil
# 在另一个终端部署合约
forge create --rpc-url http://localhost:8545 --private-key YOUR_PRIVATE_KEY src/Vault.sol:Vault
# 交互测试
cast send --rpc-url http://localhost:8545 --private-key YOUR_PRIVATE_KEY VAULT_ADDRESS "deposit()" --value 10ether
cast call --rpc-url http://localhost:8545 VAULT_ADDRESS "getBalance(address)" YOUR_ADDRESS
安全审计清单
在部署前,确保完成以下检查:
✅ 单元测试覆盖所有功能
✅ 集成测试验证交互流程
✅ 形式化验证关键属性
✅ 模糊测试边界条件
✅ 第三方安全审计
✅ 使用Slither进行静态分析
总结
Foundry是一个强大的智能合约开发和测试工具。通过结合单元测试、模糊测试和形式化验证,你可以大大提高智能合约的安全性。
我的鬃狮蜥Hash对智能合约测试也有自己的理解------它总是小心翼翼地测试蟋蟀的安全性,确保没有危险后才会享用。这和我们做智能合约测试的道理是一样的。
如果你有智能合约测试方面的问题,欢迎留言交流!我是欧阳瑞,Web3探索之路,我们一起前行!
技术栈:Foundry · Solidity · 形式化验证 · 模糊测试