Skip to content

Commit

Permalink
invariant: #6694 add preserve_state config (#7219)
Browse files Browse the repository at this point in the history
* - add preserve_state invariant config: useful for handlers that change state (e.g. using cheatcodes like roll, warp), see #6694
- active only in conjunction with fail_on_revert true

* Add test from issue 6694
  • Loading branch information
grandizzy authored Feb 28, 2024
1 parent 165ccc0 commit 27357bf
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 3 deletions.
1 change: 1 addition & 0 deletions crates/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ dictionary_weight = 80
include_storage = true
include_push_bytes = true
shrink_sequence = true
preserve_state = false

[fmt]
line_length = 100
Expand Down
7 changes: 7 additions & 0 deletions crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub struct InvariantConfig {
pub shrink_sequence: bool,
/// The maximum number of attempts to shrink the sequence
pub shrink_run_limit: usize,
/// If set to true then VM state is committed and available for next call
/// Useful for handlers that use cheatcodes as roll or warp
/// Applies only when `fail_on_revert` set to true. Use it with caution, introduces performance
/// penalty.
pub preserve_state: bool,
}

impl Default for InvariantConfig {
Expand All @@ -40,6 +45,7 @@ impl Default for InvariantConfig {
dictionary: FuzzDictionaryConfig { dictionary_weight: 80, ..Default::default() },
shrink_sequence: true,
shrink_run_limit: 2usize.pow(18_u32),
preserve_state: false,
}
}
}
Expand Down Expand Up @@ -68,6 +74,7 @@ impl InlineConfigParser for InvariantConfig {
"fail-on-revert" => conf_clone.fail_on_revert = parse_config_bool(key, value)?,
"call-override" => conf_clone.call_override = parse_config_bool(key, value)?,
"shrink-sequence" => conf_clone.shrink_sequence = parse_config_bool(key, value)?,
"preserve-state" => conf_clone.preserve_state = parse_config_bool(key, value)?,
_ => Err(InlineConfigParserError::InvalidConfigProperty(key.to_string()))?,
}
}
Expand Down
12 changes: 9 additions & 3 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,15 @@ impl<'a> InvariantExecutor<'a> {
let (sender, (address, calldata)) = inputs.last().expect("no input generated");

// Executes the call from the randomly generated sequence.
let call_result = executor
.call_raw(*sender, *address, calldata.clone(), U256::ZERO)
.expect("could not make raw evm call");
let call_result = if self.config.fail_on_revert && self.config.preserve_state {
executor
.call_raw_committing(*sender, *address, calldata.clone(), U256::ZERO)
.expect("could not make raw evm call")
} else {
executor
.call_raw(*sender, *address, calldata.clone(), U256::ZERO)
.expect("could not make raw evm call")
};

// Collect data for fuzzing from the state changeset.
let mut state_changeset =
Expand Down
1 change: 1 addition & 0 deletions crates/forge/tests/it/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub fn test_opts() -> TestOptions {
},
shrink_sequence: true,
shrink_run_limit: 2usize.pow(18u32),
preserve_state: false,
})
.build(&COMPILED, &PROJECT.paths.root)
.expect("Config loaded")
Expand Down
53 changes: 53 additions & 0 deletions crates/forge/tests/it/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ async fn test_invariant() {
None,
)],
),
(
"fuzz/invariant/common/InvariantPreserveState.t.sol:InvariantPreserveState",
vec![("invariant_preserve_state()", true, None, None, None)],
),
]),
);
}
Expand Down Expand Up @@ -326,3 +330,52 @@ async fn test_shrink(opts: TestOptions, contract_pattern: &str) {
}
};
}

#[tokio::test(flavor = "multi_thread")]
async fn test_invariant_preserve_state() {
let mut runner = runner().await;

// should not fail with default options
let mut opts = test_opts();
opts.invariant.fail_on_revert = true;
runner.test_options = opts.clone();
let results = runner
.test_collect(
&Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantPreserveState.t.sol"),
opts,
)
.await;
assert_multiple(
&results,
BTreeMap::from([(
"fuzz/invariant/common/InvariantPreserveState.t.sol:InvariantPreserveState",
vec![("invariant_preserve_state()", true, None, None, None)],
)]),
);

// same test should revert when preserve state enabled
let mut opts = test_opts();
opts.invariant.fail_on_revert = true;
opts.invariant.preserve_state = true;
runner.test_options = opts.clone();

let results = runner
.test_collect(
&Filter::new(".*", ".*", ".*fuzz/invariant/common/InvariantPreserveState.t.sol"),
opts,
)
.await;
assert_multiple(
&results,
BTreeMap::from([(
"fuzz/invariant/common/InvariantPreserveState.t.sol:InvariantPreserveState",
vec![(
"invariant_preserve_state()",
false,
Some("EvmError: Revert".into()),
None,
None,
)],
)]),
);
}
1 change: 1 addition & 0 deletions testdata/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ force = false
invariant_fail_on_revert = false
invariant_call_override = false
invariant_shrink_sequence = true
invariant_preserve_state = false
gas_limit = 9223372036854775807
gas_price = 0
gas_reports = ["*"]
Expand Down
49 changes: 49 additions & 0 deletions testdata/fuzz/invariant/common/InvariantPreserveState.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity 0.8.18;

import "ds-test/test.sol";
import "../../../cheats/Vm.sol";

struct FuzzSelector {
address addr;
bytes4[] selectors;
}

// https://github.com/foundry-rs/foundry/issues/7219

contract Handler is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function thisFunctionReverts() external {
if (block.number < 10) {} else {
revert();
}
}

function advanceTime(uint256 blocks) external {
blocks = blocks % 10;
vm.roll(block.number + blocks);
vm.warp(block.timestamp + blocks * 12);
}
}

contract InvariantPreserveState is DSTest {
Handler handler;

function setUp() public {
handler = new Handler();
}

function targetSelectors() public returns (FuzzSelector[] memory) {
FuzzSelector[] memory targets = new FuzzSelector[](1);
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = handler.thisFunctionReverts.selector;
selectors[1] = handler.advanceTime.selector;
targets[0] = FuzzSelector(address(handler), selectors);
return targets;
}

function invariant_preserve_state() public {
assertTrue(true);
}
}

0 comments on commit 27357bf

Please sign in to comment.