Foundry Cheatcode 详解:prank、warp、deal、expectRevert、expectEmit

在 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 测试」逐渐进入「会做智能合约安全测试」。

相关推荐
鹅城剑仙1 小时前
Spring Boot 3 全局异常处理与统一响应封装进阶实战
java·spring boot·后端
明月_清风1 小时前
Foundry Fuzz Testing:让测试自动寻找 Solidity Bug
后端·web3·solidity
IT_陈寒1 小时前
SpringBoot自动配置失效时我差点把电脑扔了
前端·人工智能·后端
两点王爷10 小时前
SpringBoot 项目集成 GeoServer 源码,实现地图服务发布
java·spring boot·后端
leoZ23111 小时前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端
海上彼尚13 小时前
Cursor 模型的强度实测排行
前端·人工智能·后端
GoGeekBaird14 小时前
从「跑完即销毁」到「用完再销毁」:云沙箱的一次进化
后端·github·ai编程
孫治AllenSun14 小时前
【LangChain4J-09】开发学习知识点
spring boot·后端·学习
lisin-lee-cooper14 小时前
简单聊聊 Spring 框架源码
java·后端·spring