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

fix(cheatcodes): Fix expectCall behavior #4912

Merged
merged 6 commits into from
May 12, 2023
Merged

Conversation

Evalir
Copy link
Member

@Evalir Evalir commented May 10, 2023

This is a breaking change.

Closes #4879.

Implements the intended behavior for the expectCall cheatcode, with and without count.

Behavior

expectCall will be invoked in two different modes depending on how the cheatcode was used:

  • If no count was provided, the mode will be NonCount. The behavior of this mode is:
    • It is additive. Calling expectCall(...) N times, will set a lower bound of expected calls of N, but will not revert if more than N calls are matched. This is the legacy expectCall behavior.
    • For the same calldata, cannot be overwritten with a Count expectCall cheatcode call.

Example of NonCount additive behavior:

// this will PASS
cheats.expectCall(address(target), abi.encodeWithSelector(target.add.selector, 1, 2));
cheats.expectCall(address(target), abi.encodeWithSelector(target.add.selector, 1, 2));
target.add(1, 2);
target.add(1, 2);
target.add(1, 2);

Example of how it cannot be overwritten:

// this will FAIL
cheats.expectCall(address(target), abi.encodeWithSelector(target.add.selector, 1, 2));
// Cannot overwrite a non-count expectCall cheatcode call with a count one.
cheats.expectCall(address(target), abi.encodeWithSelector(target.add.selector, 1, 2), 2);
target.add(1, 2);
target.add(1, 2);
target.add(1, 2);
  • If a count is provided, the mode will be Count. The behavior of this mode is:
    • It will match exactly N calls and revert if more are found, N being the amount of calls to match specified.
    • It is NOT additive. Calling expectCall(..., N) M times will fail. It can only be called once if it has a count, even if it is 0.
    • For the same calldata, it cannot be overwritten with any other expectCall cheatcode call, with or without count.

This essentially means that for any different calldata, there is only one expectCall cheatcode "mode" active. It is either NonCount and additive, or Count and non-additive. Note that a partial match and a full match is considered different calldata, so you could additively match partial-matches and strictly match full-matches. Tests have been added for each of these cases.

@Evalir Evalir requested review from mattsse and mds1 May 10, 2023 06:10
@Evalir
Copy link
Member Author

Evalir commented May 10, 2023

cc @PaulRBerg — you might be interested in nitpicking this to make sure we are happy with how this is implemented now!

Copy link
Member Author

@Evalir Evalir left a comment

Choose a reason for hiding this comment

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

Some helpful comments for reviewing (I will add more docs to the code nevertheless)

My hope is that we can use this same structure & adapt it for the other expect* cheatcodes with count.

for (address, expecteds) in &self.expected_calls {
for (expected, actual_count) in expecteds {
let ExpectedCallData { calldata, gas, min_gas, value, count } = expected;
for (address, calldatas) in &self.expected_calls {
Copy link
Member Author

Choose a reason for hiding this comment

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

As a note-to-self, this and all other "handlers" on the different hooks should probably be abstracted into their own function and properly documented for easy modification later.

@@ -126,7 +126,7 @@ pub struct Cheatcodes {
pub mocked_calls: BTreeMap<Address, BTreeMap<MockCallDataContext, MockCallReturnData>>,

/// Expected calls
pub expected_calls: BTreeMap<Address, Vec<(ExpectedCallData, u64)>>,
pub expected_calls: BTreeMap<Address, BTreeMap<Bytes, (ExpectedCallData, u64)>>,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is now storing this information

BTreeMap<Address, // -> Target
    BTreeMap<Bytes, // -> Calldata. Selectors are NOT deduped, so partial matches and full matches can be applied.
        (ExpectedCallData, // -> The expected call data to match. Note that the calldata is now held as the key instead of inside this struct.
        u64) // -> The actual amount of times this call was seen
    >
>

}) {
*count += 1;
{
*actual_count += 1;
Copy link
Member Author

Choose a reason for hiding this comment

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

For any of the two modes, the discrimination of how to interpret the actual count is done later—our only job here is detecting if all values properly match for the current call.

InstructionResult::Revert,
remaining_gas,
format!("Expected at least one call to {address:?} with {expected_values}, but got none")
match call_type {
Copy link
Member Author

Choose a reason for hiding this comment

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

Match depending of mode:

  • Count: count Must be strictly actual_count, if not we should fail.
  • NonCount: actual_count must be at least equal to count (therefore, if we see that count is bigger than actual_count, we should fail as we didn't see enough calls).

For reference:

  • count: Amount of times a call must be seen.
  • actual_count Amount of times a call was actually seen.

@PaulRBerg
Copy link
Contributor

Hey @Evalir, thanks very much for this PR, as well as the helpful clarifications.

The proposal looks great; it is a good idea to have two separate modes for expectCall. I only have one question about this:

For the same calldata, cannot be overwritten with a Count expectCall cheatcode call.

By "cannot", do you mean Foundry will revert, or will it silently ignore the attempt to override the calldata's expect?

@Evalir
Copy link
Member Author

Evalir commented May 10, 2023

Hey @PaulRBerg — Foundry will revert.

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

few nits.

test lgtm.
seems like this fixes a few issues @PaulRBerg

@mds1 wdyt?

@@ -220,6 +230,54 @@ fn expect_safe_memory(state: &mut Cheatcodes, start: u64, end: u64, depth: u64)
Ok(Bytes::new())
}

Copy link
Member

Choose a reason for hiding this comment

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

can we please add a few docs here what this checks exactly checks and when this is supposed to return a revert

Copy link
Member Author

Choose a reason for hiding this comment

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

done in ce54d4b

@@ -126,7 +126,7 @@ pub struct Cheatcodes {
pub mocked_calls: BTreeMap<Address, BTreeMap<MockCallDataContext, MockCallReturnData>>,

/// Expected calls
pub expected_calls: BTreeMap<Address, Vec<(ExpectedCallData, u64)>>,
pub expected_calls: BTreeMap<Address, BTreeMap<Vec<u8>, (ExpectedCallData, u64)>>,
Copy link
Member

Choose a reason for hiding this comment

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

this signature is a bit complex

I'm fine with making a wrapper type for this so it can be properly documented

Copy link
Member Author

Choose a reason for hiding this comment

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

done in ce54d4b

@Evalir Evalir requested a review from mattsse May 11, 2023 20:55
@mds1
Copy link
Collaborator

mds1 commented May 11, 2023

This is great, thanks a lot @Evalir! Let's also get the book updated with the info from the PR description which is really thorough and helpful 💯

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

lgtm, holding off on book changes so we have these before we publish.

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

Successfully merging this pull request may close these issues.

vm.expectCall unexpected behavior
4 participants