foundry-rs / foundry-rs/foundry
feat(`cheatcodes`): V1 `expectCall` behavior
- Dominant language
- Rust
- Stars
- 10.6k
- Forks
- 2.6k
- Avg merge
- 16h 46m
- Merged PRs (30d)
- 517
Description
## Motivation
Right now, `expectCall` works at the "test" level—as long as there are enough calls to the target contract at any depth level in the test with the specified selector, it will pass. While this is the current accepted behavior, it was not the intended original one. For V1, we propose actually introducing the intended behavior.
## Current behavior
The current behavior allows using expectCall this way:
```solidity
// PASS
function testThings() {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);
vm.expectCall(address(inner), abi.encodeWithSelector(inner.bar.selector), 2);
inner.bar(); // call bar directly. Counts as 1 of two calls expected.
target.baz(); // will call bar in the baz body. This counts as 2 of two calls expected. expectCall is fulfilled.
}
```
While this looks fine in this example, this is not the intended behavior, as the `expectCall` should only work for the next call. The current behavior can lead to footguns where you expect a certain call to a function to happen during the next call made, but it can instead happen later in the test and _still_ pass. An example:
```solidity
// PASS
function testThings() {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);
vm.expectCall(address(inner), abi.encodeWithSelector(inner.bar.selector), 2);
inner.bar(); // call bar directly. Counts as 1 of two calls expected.
//
// More calls throughout the test body... expectCall is still not filled.
//
target.baz(); // Finally call bar in the baz body. This counts as 2 of two calls expected. expectCall is fulfilled, after executing multiple separate calls.
}
```
## New behavior
The proposed behavior is the following: `expectCall` should match calls that are strictly subcalls of the next call.
Example of proposed functionality for V1:
```solidity
// FAIL
function testOne() {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);
vm.expectCall(address(inner), abi.encodeWithSelector(inner.bar.selector), 1);
inner.bar(); // call bar directly. This does not get counted, as this is a test-level call. It should be abstracted into another call.
target.baz(); // This still leads to a failure, as expectCall will only process the next call and all its subcalls.
}
// PASS
function testOne() {
Contract inner = new Contract();
NestedContract target = new NestedContract(inner);
vm.expectCall(address(inner), abi.encodeWithSelector(inner.bar.selector), 1);
target.baz(); // will call bar internally, therefore works.
}
contract NestedContract {
Contract public inner;
constructor(Contract _inner) {
inner = _inner;
}
function baz() public {
inner.bar(); // call bar
}
}
```
Contributor guide
Research direction
Start at the expectCall cheatcode entry point and trace how calls are currently matched across test-level calls and nested calls. Add coverage for the shown failing and passing scenarios, and consider the work complete when expectCall evaluates only the next call and its subcalls as described.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, solidity
- Domain
- blockchain, testing, tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100