Conversation
ports the positive test cases from foundry's testdata/default/cheats/ExpectRevert.t.sol into test/contracts/pass/expectRevert.sol and adds symbolic negative tests in test/contracts/fail/expectRevert.sol. count overloads, _expectCheatcodeRevert, the precompile call test, and the inline-assembly test_f case are excluded. negative tests use a symbolic argument so the failing branch produces a counterexample rather than terminating with all-branches-reverted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cheat captures length vm.frames at call time. finishFrame consumes
the expectation when the frame at that depth pops and rewrites the
frame result.
CALL-frame matches turn into a return of 8192 zero bytes
(dummySuccessReturn) so solidity's return-data decoders see a normal
return.
CREATE-frame matches keep the FrameReverted (so the parent CREATE
machinery sees a normal failure) and afterwards drop the pushed 0
from the stack and push 0x...01 (dummyCreateAddress). matches forge's
DUMMY_CREATE_ADDRESS so the LHS of a swallowed `new C()` is the same
value across both tools.
matchExpectedRevert strips the Error(string) abi prefix from the
actual buffer before comparing, so expectRevert("foo") matches an
actual whose payload is the encoded `revert("foo")`.
recordActualReverterIfFirst skips CREATE frames and the cheatcode
return frame; first non-skipped FrameReverted wins. matches forge's
behaviour of recording reverted_by only on CALL boundaries.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
finishFrame only fired the cheat-depth check on equality. an expectation consumed by no sub-call silently passed when the test function popped above the cheat depth. add a strict-less-than branch that emits an assertion-failure revert with forge's "call didn't revert at a lower depth than cheatcode call depth" message. extend pass and fail test coverage for double-set, no-subcall, body-revert, empty-string, CREATE2, intermediate-cheat, delegatecall, full-match-with-extra-args, and reverter-mismatch boundaries.
consumeExpectedRevert returned FrameReturned dummySuccessReturn for matched CALL frames, routing through finishFrameNoExpectation's FrameReturned branch and skipping revertContracts and revertSubstate. storage written by the reverted callee persisted past the swallow. now returns FrameReverted dummySuccessReturn paired with a SwallowCall flag. the FrameReverted body rolls back contracts and substate, copies the dummy bytes to caller memory and returndata, reclaims gas, and pushes 0. the SwallowCall fixup replaces the pushed 0 with 1 so the surrounding bytecode sees a normal success. flag type widens from Bool to SwallowKind = NoSwallow | SwallowCreate | SwallowCall. the previous create-swallow path becomes SwallowCreate. add prove_expectRevertCallRollsBackState (regression for the bug) and prove_expectRevertCreateRollsBackState (covers the create branch).
- decodeBuf [AbiStringType] reads the head offset in stripErrorPrefix; the previous hand-rolled skip assumed offset = 32 - return Either String Bool. short-circuits on Right False so an indeterminate axis does not mask a known no-match - partial-prefix path reads first 4 bytes via Expr.readByte, so a symbolic remainder does not block the comparison - normalize both sides of the full-match by stripping any Error(string) wrapper. covers wrapped expected vs raw actual as well as the inverse - reverter check only decides on LitAddr/LitAddr. SymAddr equality is unsound under path constraints binding fresh names to literals - hoist errorMsg out of cheatActions; expectRevertFailureBuf reuses it Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- OpRevert assigns expectedRevert.actualReverter = vm.state.contract first-wins. inlines and replaces the previous recordActualReverterIfFirst helper which skipped CREATE frames. - CREATE-frame reverts record the would-be-deployed address. CALL-frame reverts record the called contract. - foundry issue 14613: vm.expectRevert(addr) for a top-level CREATE direct revert is silently unchecked. foundry PR 14615 fixes it by gating on (curr_depth <= expected_revert.depth) and recording the outermost CREATE at boundary depth. we record the innermost reverter uniformly instead. - expectRevertFailureBuf renamed to assertionFailedBuf. used by both the mismatch path and the depth-violation path. - matchExpectedRevert returns RevertMatch (Match | Mismatch | Indeterminate String) instead of Either String Bool. combine short-circuits Mismatch over Indeterminate on either axis. - comments by dummySuccessReturn and dummyCreateAddress link to foundry's revert_handlers.rs. - new tests: prove_expectRevert_create_wrong_reverter, prove_expectRevert_call_wrapping_create_wrong_reverter (must fail); prove_expectRevertsCreate2WithReverter (uses CREATE2 for a deterministic expected address). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fold consumeExpectedRevert and currentFrameIsCreate into the matching branch of finishFrame and drop the SwallowKind ADT that existed only to glue the two functions. move poppingCheatFrame into a where helper since finishFrame is its only caller.
distinguish reverter mismatch cases in matchExpectedRevert: SymAddr-vs-SymAddr now returns Match/Mismatch on text equality, and the residual case becomes internalError since only GVar remains. reword the two unsatisfied-expectation messages: "expected call did not revert" when the matching call returns successfully, and "expected revert never occurred" when the cheat scope exits without any matching call.
msooseth
approved these changes
May 7, 2026
Collaborator
msooseth
left a comment
There was a problem hiding this comment.
Actually, really nice!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Implements the following subset of the forge
expectRevertfamily of cheatcodes:expectRevert()expectRevert(bytes)expectRevert(bytes4)expectRevert(address)expectRevert(bytes4,address)expectRevert(bytes,address)expectPartialRevert(bytes4)expectPartialRevert(bytes4,address)I did my best to stay forge compatible here, but I made an exception regarding the handling of the variants that expect an address in relation to reverts within calls to
create, where I instead implemented a consistent semantics and filed a (partially fixed) bug against forge.Checklist