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.

image

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.

image

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.

image

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.

image

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.

Simeon Cholakov
Simeon Cholakov

Security Researcher