contract TokenTest is Test {
function testTransfer() public {
vm.prank(address(0));
Token token = new Token(1000);
token.transfer(address(1), 100);
assertEq(token.balanceOf(address(1)), 100);
}
}
Solidity 测试的优势在于它可以直接访问合约内部状态,无需通过 ABI 接口。对于复杂的内部逻辑测试,这种方式更直观、更高效。
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.operations import Operation, SolidityCall
class MyCustomDetector(AbstractDetector):
"""
自定义检测器示例
"""
ARGUMENT = 'my-custom-detector' # 命令行参数
HELP = '描述检测器功能' # 帮助文本
IMPACT = DetectorClassification.HIGH # 影响等级
CONFIDENCE = DetectorClassification.HIGH # 置信度
WIKI = 'https://github.com/crytic/slither/wiki/Adding-a-new-detector'
def _detect(self) -> list:
"""
主检测逻辑
"""
results = []
for contract in self.compilation_unit.contracts:
# 检测逻辑
findings = self._analyze_contract(contract)
if findings:
results.append(self._create_result(contract, findings))
return results
def _analyze_contract(self, contract):
"""分析单个合约"""
findings = []
for function in contract.functions:
# 检查特定模式
if self._is_vulnerable(function):
findings.append({
'function': function,
'vulnerability': '具体漏洞描述'
})
return findings
def _is_vulnerable(self, function) -> bool:
"""判断函数是否包含漏洞"""
# 实现具体检测逻辑
return False
def _create_result(self, contract, findings):
"""生成检测结果"""
info = ['自定义漏洞描述:\n']
for finding in findings:
info.append(f' - {finding["function"].name}: {finding["vulnerability"]}\n')
return self.generate_result(info)
6.2 实用检测器示例
以下是几个常见自定义检测器的实现:
python
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.slithir.operations import Binary, BinaryType
from slither.core.expressions import Identifier
class UncheckedReturnValueDetector(AbstractDetector):
"""
检测未检查的外部调用返回值
"""
ARGUMENT = 'unchecked-external'
HELP = '检测未检查的外部调用返回值'
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
def _detect(self) -> list:
results = []
for contract in self.compilation_unit.contracts:
for function in contract.functions:
findings = self._check_function(function)
if findings:
results.append(self._create_result(function, findings))
return results
def _check_function(self, function):
"""检查函数中的外部调用"""
findings = []
for node in function.nodes:
for ir in node.irs:
# 检查是否存在外部调用
if self._is_external_call(ir):
# 检查是否检查了返回值
if not self._checks_return_value(ir, function):
findings.append({
'node': node,
'call': ir
})
return findings
def _is_external_call(self, ir) -> bool:
"""判断是否为外部调用"""
return hasattr(ir, 'destination') and ir.destination != ir.contract
def _checks_return_value(self, call_ir, function) -> bool:
"""检查是否检查了返回值"""
# 分析后续节点是否使用了call的结果
return False # 简化实现
def _create_result(self, function, findings):
"""生成检测结果"""
info = [
f'Unchecked return value in function {function.name}:\n',
f' External call at {findings[0]["node"]}\n'
]
return self.generate_result(info)
6.3 整数溢出检测器
python
class IntegerOverflowDetector(AbstractDetector):
"""
检测整数溢出漏洞
"""
ARGUMENT = 'integer-overflow-custom'
HELP = '检测可能的整数溢出'
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.MEDIUM
def _detect(self) -> list:
results = []
for contract in self.compilation_unit.contracts:
for function in contract.functions:
for node in function.nodes:
for ir in node.irs:
if isinstance(ir, Binary):
if self._is_addition_or_multiplication(ir.type):
if not self._has_safemath(ir, node, function):
results.append(self._create_result(ir, node, function))
return results
def _is_addition_or_multiplication(self, binary_type):
"""判断是否为加法或乘法"""
return binary_type in [
BinaryType.ADDITION,
BinaryType.MULTIPLICATION
]
def _has_safemath(self, ir, node, function) -> bool:
"""检查是否使用了SafeMath"""
for prev_node in node.immediate_predecessors:
for prev_ir in prev_node.irs:
if isinstance(prev_ir, SolidityCall):
if 'safe' in str(prev_ir).lower():
return True
return False
def _create_result(self, ir, node, function):
"""生成检测结果"""
return self.generate_result([
f'Potential integer overflow in {function.name}:\n',
f' Operation: {ir}\n',
f' Node: {node}\n'
])
// test/MyToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "@forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken public token;
address public owner;
address public user;
function setUp() public {
owner = address(0x1);
user = address(0x2);
// 部署合约
token = new MyToken(owner, 1000 ether);
}
function testInitialSupply() public {
assertEq(token.totalSupply(), 1000 ether);
}
function testBalanceOfOwner() public {
assertEq(token.balanceOf(owner), 1000 ether);
}
function testTransfer() public {
vm.prank(user);
vm.expectRevert();
token.transfer(owner, 100 ether);
}
function testSuccessfulTransfer() public {
vm.prank(owner);
token.transfer(user, 100 ether);
assertEq(token.balanceOf(user), 100 ether);
assertEq(token.balanceOf(owner), 900 ether);
}
}
运行测试:
bash
# 运行所有测试
forge test
# 运行指定测试
forge test --match-test testTransfer
# 显示日志输出
forge test -vv
# 更详细的日志
forge test -vvvv
contract FuzzTest is Test {
Vault public vault;
function setUp() public {
vault = new Vault();
vm.deal(address(this), 100 ether);
vault.deposit{value: 100 ether}();
}
// fuzz 测试:自动生成 uint256 输入
function testFuzzWithdraw(uint256 amount) public {
// 限定有效范围
amount = bound(amount, 0, 100 ether);
uint256 balanceBefore = address(this).balance;
vault.withdraw(amount);
uint256 balanceAfter = address(this).balance;
assertEq(balanceAfter - balanceBefore, amount);
}
// bound 辅助函数:限制随机数范围
function testFuzzWithdrawBounded(uint256 amount) public pure {
// 自动限制在 1-1000 范围
amount = bound(amount, 1, 1000);
assertTrue(amount >= 1 && amount <= 1000);
}
}
运行fuzz测试:
bash
# 默认运行,会使用合理的测试次数
forge test --match-test testFuzz
# 增加测试次数
forge test --match-test testFuzz --fuzz-run 10000
3.4 Invariant Testing(不变量测试)
不变量测试验证合约在各种操作序列下始终保持某些属性。
solidity
contract InvariantTest is Test {
TargetContract public target;
Handler public handler;
function setUp() public {
target = new TargetContract();
handler = new Handler(target);
// 设置 handler
target.setHandler(handler);
// 配置不变式测试
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = Handler.deposit.selector;
selectors[1] = Handler.withdraw.selector;
target.setSelectors(selectors);
}
// 不变量:totalDeposits >= 合约余额
function invariantConservation() public {
assertGe(target.totalDeposits(), address(target).balance);
}
}
contract Handler {
TargetContract public target;
function deposit() public payable {
target.deposit{value: msg.value}();
}
function withdraw(uint256 amount) public {
target.withdraw(amount);
}
}
// Foundry: Solidity测试
contract TokenTest is Test {
function testTransfer() public {
token.transfer(recipient, amount);
assertEq(token.balanceOf(recipient), amount);
}
}
6.2 配置文件对照
Hardhat配置 → Foundry配置:
toml
# foundry.toml
[profile.default]
src = "src" # 源码目录
out = "out" # 编译输出
libs = ["lib"] # 库目录
test = "test" # 测试目录
script = "script" # 脚本目录
# RPC配置
rpc_url = "https://eth-mainnet.g.alchemy.com/v2/KEY"
# Etherscan API(用于自动验证)
etherscan_api_key = "YOUR_KEY"
# 编译器设置
solc_version = "0.8.24"
optimizer = true
optimizer_runs = 200
# Gas报告
gas_reports = ["*"]
# 铸造账户(测试用)
accounts = { mnemonic = "test test test test test test test test test test test junk" }
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract PausableToken is ERC20, Ownable, ERC20Pausable {
constructor() ERC20("PausableToken", "PTK") {}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
}
当合约被暂停时,所有转账操作都会失败。这给了开发者时间来调查和修复问题,也保护了用户的资产安全。
SafeCast安全类型转换
在处理数值运算时,防止溢出和下溢至关重要:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
contract SafeMathExample {
using SafeCast for uint256;
using SafeCast for int256;
// 安全地将uint256转换为int256
function toSigned(uint256 unsigned) public pure returns (int256) {
return unsigned.toInt256();
}
// 安全地将int256转换为uint256
function toUnsigned(int256 signed) public pure returns (uint256) {
return signed.toUint256(); // 如果值为负会revert
}
// 安全截断
function truncate(uint256 value) public pure returns (uint128) {
return value.toUint128(); // 超出uint128范围会revert
}
}