在 Solidity 测试中,除了 assert 之外,还有一个非常重要的概念:
Cheatcode
Cheatcode 可以理解成 Foundry 提供给测试代码的「作弊工具」。
例如真实区块链中:
scss
msg.sender 不能随便修改
block.timestamp 不能随便修改
ETH balance 不能随便修改
但是测试的时候,我们经常需要模拟这些情况。
Foundry 就提供了:
scss
vm.prank()
vm.warp()
vm.deal()
vm.expectRevert()
vm.expectEmit()
本文重点介绍这 5 个最常用的 Cheatcode。
一、Cheatcode 到底是什么?
Foundry 测试通常会继承:
python
import {Test} from "forge-std/Test.sol";
contract MyTest is Test {
}
继承 Test 后,就可以使用:
vm
例如:
ini
vm.prank(alice);
这里的 vm 可以理解成 Foundry 提供的测试控制器。
它可以帮助我们修改测试环境。
例如:
markdown
vm.prank()
↓
修改调用者
vm.warp()
↓
修改区块时间
vm.deal()
↓
修改账户余额
vm.expectRevert()
↓
预期交易失败
vm.expectEmit()
↓
预期 Event
所以 Cheatcode 的核心作用就是:
人为控制测试环境,模拟真实区块链中各种难以构造的场景。
二、vm.prank:模拟不同用户
这是 Solidity 测试中非常常用的 Cheatcode。
假设有一个只有 Owner 可以调用的合约:
ini
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
function adminAction() public view returns (bool) {
require(msg.sender == owner, "not owner");
return true;
}
}
我们测试的时候可能需要模拟:
sql
Owner
User
Attacker
不同身份。
这时候就可以使用:
ini
vm.prank(alice);
例如:
ini
function test_Owner() public {
address alice = makeAddr("alice");
Ownable ownable = new Ownable();
vm.prank(alice);
// 当前调用的 msg.sender 就是 alice
ownable.adminAction();
}
prank 的核心作用就是:
ini
下一次调用
↓
msg.sender = 指定地址
三、startPrank:连续模拟用户
如果只需要修改一次调用:
ini
vm.prank(alice);
就够了。
但如果需要连续执行多次调用:
ini
vm.startPrank(alice);
contract.foo();
contract.bar();
contract.baz();
vm.stopPrank();
那么这几个调用中的:
msg.sender
都会是:
alice
可以简单理解为:
markdown
prank
↓
修改一次
startPrank
↓
开始模拟
↓
多个调用
↓
stopPrank
↓
结束模拟
四、prank 最常见的使用场景
例如一个 Token:
scss
function transfer(address to, uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
...
}
我们想测试:
Alice → Bob
而不是测试合约自己 → Bob。
可以:
ini
address alice = makeAddr("alice");
address bob = makeAddr("bob");
vm.startPrank(alice);
token.transfer(bob, 100);
vm.stopPrank();
这样合约内部看到:
ini
msg.sender == alice
这就是 prank 最重要的用途:
模拟不同用户与合约进行交互。
五、vm.warp:模拟时间
很多智能合约都依赖:
scss
block.timestamp
例如:
csharp
function claim() public {
require(block.timestamp >= startTime, "not started");
...
}
如果:
ini
startTime = 7 天以后
我们不可能真的等 7 天再测试。
这时候:
scss
vm.warp()
就非常有用。
例如:
scss
function test_ClaimAfter7Days() public {
vm.warp(block.timestamp + 7 days);
// 此时区块时间已经向后移动 7 天
contract.claim();
}
也就是说:
yaml
真实时间:
2026-09-10
↓
等待 7 天
↓
2026-09-17
测试中:
yaml
2026-09-10
↓
vm.warp(7 days)
↓
立即变成 2026-09-17
不需要真正等待。
六、warp 可以测试哪些东西?
很多 Web3 项目都会涉及时间:
ICO 开始时间
NFT Mint 时间
Staking 锁定时间
Vesting 解锁时间
投票截止时间
Token Claim
空投领取
例如:
scss
function test_Vesting() public {
vm.warp(block.timestamp + 30 days);
uint256 amount = vesting.claimable();
assertEq(amount, expectedAmount);
}
因此可以把:
vm.warp
理解成:
时间机器。
七、vm.deal:给账户充值 ETH
测试 DeFi 合约时经常遇到一个问题:
Alice 没有 ETH
但是我们又需要让 Alice:
css
payableFunction{value: 1 ether}();
怎么办?
使用:
ini
vm.deal(alice, 10 ether);
例如:
scss
function test_Deposit() public {
address alice = makeAddr("alice");
vm.deal(alice, 10 ether);
vm.prank(alice);
bank.deposit{value: 1 ether}();
assertEq(address(bank).balance, 1 ether);
}
执行:
ini
vm.deal(alice, 10 ether);
之后:
markdown
Alice
ETH Balance
↓
10 ETH
所以:
scss
vm.deal()
主要用于:
给测试账户设置 ETH 余额。
八、deal 和 prank 经常一起使用
这两个 Cheatcode 在 DeFi 测试中非常常见。
例如:
ini
address alice = makeAddr("alice");
vm.deal(alice, 100 ether);
vm.prank(alice);
bank.deposit{value: 10 ether}();
逻辑就是:
markdown
创建 Alice
↓
给 Alice 100 ETH
↓
模拟 Alice
↓
Alice 存入 10 ETH
因此可以记成:
ini
prank = 谁在操作?
deal = 他有多少钱?
这个理解非常实用。
九、vm.expectRevert:测试错误情况
智能合约测试不能只测试:
成功
还需要测试:
失败
例如:
ini
function withdraw(uint256 amount) public {
require(amount <= balance, "insufficient balance");
balance -= amount;
}
如果余额:
100 ETH
却取:
200 ETH
应该 Revert。
测试:
scss
function test_RevertWhen_WithdrawTooMuch() public {
vm.expectRevert();
bank.withdraw(200);
}
意思是:
我预计下一次调用会 Revert
如果真的 Revert:
测试通过
如果没有 Revert:
测试失败
十、expectRevert 还可以检查错误信息
例如:
javascript
require(
amount <= balance,
"insufficient balance"
);
测试:
ini
vm.expectRevert(
bytes("insufficient balance")
);
bank.withdraw(200);
这样测试的不只是:
交易失败
还验证:
为什么失败
这对于智能合约测试非常重要。
十一、自定义 Error
现在 Solidity 项目中经常使用:
csharp
error InsufficientBalance();
例如:
scss
error InsufficientBalance();
function withdraw(uint256 amount) public {
if (amount > balance) {
revert InsufficientBalance();
}
balance -= amount;
}
测试也可以针对具体 Error:
ini
vm.expectRevert(
InsufficientBalance.selector
);
bank.withdraw(200);
这样可以精确测试:
预期:
InsufficientBalance
实际:
InsufficientBalance
十二、vm.expectEmit:测试 Event
智能合约经常通过 Event 通知前端:
css
event Transfer(
address indexed from,
address indexed to,
uint256 amount
);
例如:
scss
function transfer(
address to,
uint256 amount
) public {
...
emit Transfer(
msg.sender,
to,
amount
);
}
那么测试时,我们不仅要检查:
余额是否变化
还应该检查:
vbnet
Transfer Event
可以使用:
ini
vm.expectEmit();
例如:
ini
function test_TransferEvent() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
vm.prank(alice);
vm.expectEmit(true, true, false, true);
emit Transfer(alice, bob, 100);
token.transfer(bob, 100);
}
核心逻辑:
vbnet
expectEmit
↓
告诉 Foundry:
下一次调用应该产生指定 Event
↓
执行 transfer
↓
检查 Event
十三、expectEmit 的四个参数
经常会看到:
arduino
vm.expectEmit(
true,
true,
false,
true
);
这几个参数对应 Event 的:
kotlin
topic1
topic2
topic3
data
对于:
css
event Transfer(
address indexed from,
address indexed to,
uint256 amount
);
其中:
markdown
from
↓
indexed
to
↓
indexed
amount
↓
普通 data
所以:
arduino
vm.expectEmit(true, true, false, true);
表示检查:
kotlin
第一个 indexed 参数
第二个 indexed 参数
第三个 indexed 参数
普通 data
具体来说:
arduino
true → 检查
false → 不检查
这也是为什么写 Event 测试时,需要理解 indexed。
十四、五个 Cheatcode 放在一起
到这里,可以把它们总结成:
| Cheatcode | 作用 |
|---|---|
vm.prank() |
模拟调用者 |
vm.startPrank() |
连续模拟调用者 |
vm.warp() |
修改区块时间 |
vm.deal() |
设置 ETH 余额 |
vm.expectRevert() |
预期交易失败 |
vm.expectEmit() |
预期 Event |
其实可以用几个问题来记:
markdown
谁在调用?
↓
prank
什么时候调用?
↓
warp
账户有多少钱?
↓
deal
调用应该失败吗?
↓
expectRevert
应该产生什么事件?
↓
expectEmit
十五、组合起来写一个测试
实际项目中,这几个 Cheatcode 经常一起出现。
例如:
scss
function test_Claim() public {
address alice = makeAddr("alice");
// 1. 给 Alice ETH
vm.deal(alice, 10 ether);
// 2. 模拟 Alice
vm.startPrank(alice);
// 3. 模拟时间过去 30 天
vm.warp(block.timestamp + 30 days);
// 4. 执行操作
staking.claim();
vm.stopPrank();
}
这里就同时使用了:
deal
↓
准备资金
prank
↓
准备用户
warp
↓
准备时间
claim
↓
执行测试
这才是 Cheatcode 在实际项目中的使用方式。
十六、一个更完整的测试思路
写智能合约测试时,可以按照下面的方式构造环境:
scss
① 准备账户
↓
makeAddr()
② 准备资金
↓
vm.deal()
③ 准备身份
↓
vm.prank()
④ 准备时间
↓
vm.warp()
⑤ 设置预期结果
↓
expectRevert / expectEmit
⑥ 调用合约
↓
contract.xxx()
⑦ 验证状态
↓
assertEq()
例如:
scss
function test_Withdraw() public {
address alice = makeAddr("alice");
vm.deal(alice, 10 ether);
vm.startPrank(alice);
bank.deposit{value: 5 ether}();
vm.expectEmit();
bank.withdraw(1 ether);
vm.stopPrank();
assertEq(
bank.balanceOf(alice),
4 ether
);
}
这就已经非常接近真实 Web3 项目的测试代码了。
十七、Cheatcode 的本质
如果从更高的层次看:
markdown
普通单元测试
↓
输入
↓
函数
↓
输出
而智能合约测试更加复杂:
markdown
测试环境
│
┌───────────┼───────────┐
↓ ↓ ↓
用户 时间 资金
│ │ │
prank warp deal
│ │ │
└───────────┼───────────┘
↓
合约调用
↓
┌────────┴────────┐
↓ ↓
Revert Event
↓ ↓
expectRevert expectEmit
所以 Cheatcode 并不是简单的「作弊」。
它真正解决的是:
如何构造一个我们想要的区块链测试环境。
十八、最后总结
Foundry 的 Cheatcode 是 Solidity 测试中非常重要的一部分。
最值得优先掌握的几个:
scss
vm.prank()
vm.startPrank()
vm.stopPrank()
vm.warp()
vm.deal()
vm.expectRevert()
vm.expectEmit()
可以记成一句话:
prank 控制人,warp 控制时间,deal 控制钱,expectRevert 控制异常,expectEmit 控制事件。
掌握这些之后,再继续学习:
markdown
Cheatcode
↓
Fuzz Testing
↓
Invariant Testing
↓
Fork Testing
↓
DeFi 安全测试
就能从「会写 Solidity 测试」逐渐进入「会做智能合约安全测试」。