Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cheatcodes): add ability to ignore (multiple) specific and partial reverts in fuzz and invariant tests #9179

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

emo-eth
Copy link
Contributor

@emo-eth emo-eth commented Oct 23, 2024

Motivation

As raised in #4271, sometimes the fuzzer dictionary will uncover real but irrelevant errors in contract code, especially when testing against stateful forks. Runs that encounter anticipated errors should be thrown out and count towards the global assume counter.

Closes #4271.

Solution

This PR adds several overloaded and related function definitions to vm.assumeNoRevert, introduced in #8780, as follows:

    /// Discard this run's fuzz inputs and generate new ones if next call reverted with data starting with the given revert data. Call more than once to add multiple reasons.
    #[cheatcode(group = Testing, safety = Safe)]
    function assumeNoRevert(bytes4 revertData) external pure;

    /// Discard this run's fuzz inputs and generate new ones if next call reverted with the given revert data. Call more than once to add multiple reasons.
    #[cheatcode(group = Testing, safety = Safe)]
    function assumeNoRevert(bytes calldata revertData) external pure;

    /// Discard this run's fuzz inputs and generate new ones if next call reverted with data starting with the given revert data. Call more than once to add multiple reasons.
    #[cheatcode(group = Testing, safety = Safe)]
    function assumeNoRevert(bytes4 revertData, address reverter) external pure;

    /// Discard this run's fuzz inputs and generate new ones if next call reverted with the given revert data. Call more than once to add multiple reasons.
    #[cheatcode(group = Testing, safety = Safe)]
    function assumeNoRevert(bytes calldata revertData, address reverter) external pure;

The reverter param ensures a revert comes from a specific called contract, per #8770, and the PartialRevert methods allow matching on selectors with arbitrary extra data, per #8763.

Behavior

  • it should be possible to to reject any specific error without data (vm.assumeNoRevert(bytes4))
  • it should be possible to reject any specific error with arbitrary data (vm.assumeNoRevert(bytes))
  • it should be possible to reject any specific of error regardless of extra data (vm.assumeNoPartialRevert(bytes4))
  • it should be possible to reject multiple errors that a single call may produce (multiple subsequent calls to any of the vm.assumeNoRevert definitions that accept params)
  • it should be possible to restrict these rejections to a specific reverter (the optional overloaded address reverter param)
  • anticipated reverts should reset after next non-cheatcode external call
  • combining a catch-all vm.assumeNoRevert() with a vm.assumeNoRevert(args) call should result in an error (currently, the inverse correctly errors, but the ordering provided here will result in a \vm.assume` rejected too many inputserror due to how cheatcode errors are encoded, see note aboutvm.expectRevert` below)

Questions

  • Should these functions be pure and Safe? I see that expectRevert is not pure, and is marked as Unsafe, but assumeNoRevert() is marked pure and Safe.
  • Is the behavior of calling vm.assumeNoRevert multiple times to build up anticipated reverts per-call acceptable, or should it be forced to take an array of structs with revert parameters, since I think aggregating parameters from separate calls is new behavior (correct me if I'm wrong).
  • I've also left a few TODOs in the code with questions, which I will try to surface via comments here.
  • expectRevert() without args seems to not play strangely with cheatcode errors – eg, vm.expectRevert() will swallow a cheatcode revert, including those thrown by vm.assumeNoRevert and subsequent calls to vm.expectRevert(). This seems like a separate bug that I can log if we agree there.

@emo-eth emo-eth force-pushed the emo/expand-assume-no-revert branch 2 times, most recently from 23bb2c6 to 3d057c1 Compare October 24, 2024 00:33
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good! left couple of comments, going to move back in draft until fix added

crates/cheatcodes/src/inspector.rs Outdated Show resolved Hide resolved
crates/cheatcodes/src/inspector.rs Outdated Show resolved Hide resolved
crates/cheatcodes/src/test/revert.rs Outdated Show resolved Hide resolved
crates/forge/tests/cli/test_cmd.rs Outdated Show resolved Hide resolved
@grandizzy grandizzy marked this pull request as draft October 24, 2024 05:56
@grandizzy
Copy link
Collaborator

@mds1 would be great to have your thoughts re new proposed cheatcodes. Thanks!

@emo-eth emo-eth force-pushed the emo/expand-assume-no-revert branch from 59efe2d to 3800b7a Compare November 2, 2024 00:02
@emo-eth
Copy link
Contributor Author

emo-eth commented Nov 2, 2024

@grandizzy should be in a good state now – apologies, had a hectic week. let me know if anything else looks off.

@jenpaff jenpaff marked this pull request as ready for review November 4, 2024 10:05
@jenpaff
Copy link
Collaborator

jenpaff commented Nov 4, 2024

@grandizzy should be in a good state now – apologies, had a hectic week. let me know if anything else looks off.

moving PR to ready for review

Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, left couple of comments.

crates/forge/tests/cli/test_cmd.rs Outdated Show resolved Hide resolved
crates/cheatcodes/spec/src/vm.rs Outdated Show resolved Hide resolved
crates/cheatcodes/src/test/revert_handlers.rs Show resolved Hide resolved
crates/cheatcodes/src/test/assume.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, looks good! I pushed a change to simplify test and have one more nit re assume_no_revert fn. @zerosnacks @yash-atreya @klkvr pls chime in when time permits. thank you!

crates/cheatcodes/src/test/assume.rs Show resolved Hide resolved
@emo-eth emo-eth requested a review from grandizzy November 6, 2024 22:57
@emo-eth emo-eth force-pushed the emo/expand-assume-no-revert branch from ac4a7d5 to c215515 Compare November 7, 2024 05:23
Copy link
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm, thank you! Pending additional review before merging

@mds1
Copy link
Collaborator

mds1 commented Nov 7, 2024

In general UX LGTM! One thing is let's make sure the behavior between this and the various expectRevert cheats is consistent, e.g. if you pass a 4byte selector does it only match if the revert data is exact, or do partials match also? Given that this cheat is assumeNoRevert for partial match I think it's inconsistent as spec'd in the PR description

https://github.com/foundry-rs/forge-std/blob/da591f56d8884c5824c0c1b3103fbcfd81123c4c/src/Vm.sol#L2102-L2124

@emo-eth
Copy link
Contributor Author

emo-eth commented Nov 8, 2024

In general UX LGTM! One thing is let's make sure the behavior between this and the various expectRevert cheats is consistent, e.g. if you pass a 4byte selector does it only match if the revert data is exact, or do partials match also? Given that this cheat is assumeNoRevert for partial match I think it's inconsistent as spec'd in the PR description

https://github.com/foundry-rs/forge-std/blob/da591f56d8884c5824c0c1b3103fbcfd81123c4c/src/Vm.sol#L2102-L2124

@mds1 apologies, i can update the description (pending final decision) – @grandizzy suggested having the default behavior to accept partial matches.

having the granularity of assumeNoRevert vs assumeNoPartialRevert would be useful in the scenario of colliding selectors – but that's pretty rare unless intentional. i am having a hard time thinking of cases where a test might be pulling in (arbitrary) external or random revert bytes that might have collisions – but there are possibly some, and it might be worth having the disambiguation.

i do appreciate the explicitness, though i do think assumeNoRevert is already kind of a counter-intuitive name, and assumeNoPartialRevert adds onto that complexity. eg assumeNoRevertPartial is maybe slightly less confusing, but breaks with the expectPartialRevert naming convention...

happy to add back in if there's consensus

@mds1
Copy link
Collaborator

mds1 commented Nov 8, 2024

Thanks for those details, so my thoughts are:

  • We already have expectRevert(bytes4) and expectPartialRevert(bytes4). To this point—"would be useful in the scenario of colliding selectors – but that's pretty rare unless intentional"—I'm unsure of the motivation for this split
  • The least surprising assume behavior would therefore be analogous meaning assumeNoRevert(bytes4) and assumeNoPartialRevert(bytes4)
  • It sounds like there are some breaking change considerations here, which I am not fully up to speed on, in which case that is suitable rationale for having inconsistent behavior between the expect and assume cheats
  • Therefore I'll defer to you and @grandizzy to decide
  • But, we should track this and unify / simplify for foundry v1

@grandizzy
Copy link
Collaborator

thanks both for comments! the expectPartialRevert was added when expectRevert was already in place and used see #3725 (comment) and Paul's comment

This may warrant a new VM cheatcode, to ensure backwards compatibility and avoid triggering false positives for people who assume the current vm.expectRevert cheatcode to look for an exact ABI match.

For assumeNoRevert there's no breaking change consideration (as we don't have them yet) that's why I proposed to avoid adding partial one but consistency is a good point too. Would like to hear more opinions, @zerosnacks maybe you could share your thoughts?

@zerosnacks
Copy link
Member

Thanks @emo-eth for your PR!

Is the behavior of calling vm.assumeNoRevert multiple times to build up anticipated reverts per-call acceptable, or should it be forced to take an array of structs with revert parameters, since I think aggregating parameters from separate calls is new behavior (correct me if I'm wrong).

I would prefer we use an array parameter to express the intent of checking against multiple cases in line
with existing behavior (mockCalls, makePersistent, revokePersistent, etc..). Stateful stacking as proposed is, as mentioned, a new kind of behavior I think we should avoid.

I agree that expectPartialRevert exists for a backwards compatibility reason, no need to introduce a parallel assumeNoPartialRevert in my opinion. This can always be added later on in a non-breaking way if there is a specific request for it.

Should these functions be pure and Safe? I see that expectRevert is not pure, and is marked as Unsafe, but assumeNoRevert() is marked pure and Safe.

Unsafe generally refers to either it persistently touching EVM settings / state or writing to a file. Do you have an opinion in this regard @DaniPopes?

@emo-eth
Copy link
Contributor Author

emo-eth commented Nov 25, 2024

@zerosnacks @grandizzy apologies for delay here. Here's a rethought interface that takes an array of structs, let me know your thoughts:

    /// Represents a "potential" revert reason from a single subsequent call when using vm.assumeNoPotentialReverts.
    /// Reverts that match will result in a FOUNDRY::ASSUME rejection, whereas unmatched reverts will be surfaced
    /// as normal.
    struct PotentialRevert {
        /// The allowed origin of the revert opcode; address(0) allows reverts from any address
        address reverter;
        /// When true, only matches on the beginning of the revert data, otherwise, matches on entire revert data
        bool partialMatch;
        /// The data to use to match encountered reverts
        bytes revertData;
    }

    /// Discard this run's fuzz inputs and generate new ones if next call reverts with the potential revert parameters
    #[cheatcode(group = Evm, safety = Safe)]
    function assumeNoPotentialRevert(PotentialRevert calldata potentialRevert) external pure;

    /// Discard this run's fuzz inputs and generate new ones if next call reverts with the any of the potential revert parameters
    #[cheatcode(group = Evm, safety = Safe)]
    function assumeNoPotentialReverts(PotentialRevert[] calldata potentialReverts) external pure;

I really don't like the assumeNoRevert name, so went with something different; I think the change in signature calls for a change in name as well.

I am probably overthinking this, but here's my logic for the "NoPotential" name (could also be "anticipated?" "Specific?")

  • as with all assume cheatcodes, we want to skip/reject inputs ("assume no")
  • there may be only certain kinds of reverts that we want to skip/reject ("assume no potential/anticipated/specific")

@zerosnacks
Copy link
Member

zerosnacks commented Nov 28, 2024

Hi @emo-eth, no worries!

I honestly don't mind the previous assumeNoRevert naming.

Regarding the proposed interface, it looks good. I general prefer a single assumeNoRevert(PotentialRevert ..) and have the array variant be an overload (assumeNoRevert(PotentialRevert[] ..). This is in line with other assertions.

    /// Represents a "potential" revert reason from a single subsequent call when using `vm.assumeNoReverts`.
    /// Reverts that match will result in a FOUNDRY::ASSUME rejection, whereas unmatched reverts will be surfaced
    /// as normal.
    struct PotentialRevert {
        /// The allowed origin of the revert opcode; address(0) allows reverts from any address
        address reverter;
        /// When true, only matches on the beginning of the revert data, otherwise, matches on entire revert data
        bool partialMatch;
        /// The data to use to match encountered reverts
        bytes revertData;
    }

    /// Discard this run's fuzz inputs and generate new ones if next call reverts with the potential revert parameters.
    #[cheatcode(group = Evm, safety = Safe)]
    function assumeNoRevert(PotentialRevert calldata potentialRevert) external pure;

    /// Discard this run's fuzz inputs and generate new ones if next call reverts with the any of the potential revert parameters.
    #[cheatcode(group = Evm, safety = Safe)]
    function assumeNoRevert(PotentialRevert[] calldata potentialReverts) external pure;

@sambacha
Copy link
Contributor

can this get merged or what is the hold up?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

feat(cheatcodes): add ability to exclude certain custom errors and revert reason strings from failing tests
6 participants