Blockchain security isn't optional.
Protect your smart contracts and DeFi protocols with Three Sigma, a trusted security partner in blockchain audits, smart contract vulnerability assessments, and Web3 security.
Get a Quote Today
Introduction
Welcome again to our cheatcode deep-dive. If you missed it, last time we learned to fake callers with vm.prank
; now we’ll sharpen our test arsenal with expectation directives, asserting reversions, events, and external calls so your contracts behave exactly as intended.
vm.expectRevert
: fail-fast negative testing
vm.expectRevert
is the single-line guard you place immediately before a call that must throw. If you supply no argument it accepts any revert; if you supply a bytes string or custom-error selector it insists on that exact reason. The directive applies only to the very next external call, so you always write it just above the statement under test.

Because only the next call is watched, place the directive right above it. Combine with vm.prank
or vm.roll
when the revert depends on caller or block-state.
Why keep it terse? In practice expectRevert
is a single-line guard that either passes silently or halts the run; its semantics are straightforward.
vm.expectEmit
: verifying events and topics
When you care that a function emits the correct log you use vm.expectEmit
. Internally Foundry records a template event, then runs the real call, then compares the actual log stream against the template in order. The four boolean flags tell Forge which indexed topics and whether the non-indexed data must match, and an optional fifth argument fixes the expected emitter address.

You may emit several templates back-to-back; Forge treats them as an ordered subsequence so extra logs in between are allowed while re-ordering is not. This makes it easy to verify complex multi-event flows such as ERC-4626 deposit and withdraw pairs, NFT batch mints, or proxy-upgrade beacons that must announce both Upgraded
and AdminChanged
in the correct order.
vm.expectCall
: asserting external interactions
vm.expectCall
watches for low-level CALL
, DELEGATECALL
, or STATICCALL
to a target contract with exact calldata, optional msg.value
, and optional gas constraints. Place the directive before the function that is supposed to trigger the call; if the next external interaction does not match, the test aborts.

The delegate and static variants follow the same signature (expectDelegateCall
, expectStaticCall
). Together they let you guarantee that a vault forwards funds only once, that a proxy really delegates to the implementation, or that a fallback does not sneak in extra calls. If you need to watch for the same call multiple times pass the count
parameter; Forge will fail if the tally does not match exactly.
Forge-std assertions: quick value checks
Alongside cheat-codes the standard library gives Solidity-native helpers such as assertEq
, assertGt
, assertApproxEqAbs
, assertTrue
, and a plain fail()
. They revert with clear diagnostics and integrate with Forge traces, which means you rarely write raw require
statements in tests.

Putting it all together
A comprehensive assertion layer usually starts with std-assertions for numeric state, layers expectRevert
for negative paths, applies expectEmit
to guarantee log integrity, and finishes with expectCall
to nail down side-effects. Because every directive scopes to the next action the intent stays obvious and there is no hidden global state. By combining these primitives with vm.prank
to spoof callers, vm.roll
and vm.warp
to time-travel, and vm.deal
to fund accounts, you can express multi-actor, time-dependent, event-rich scenarios in a handful of self-documenting lines that fail loudly the moment any assumption is broken.