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(invariant): fuzz with values from events and return values #7666

Merged
merged 30 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9cb682d
feat(invariant): scrape return values and add to fuzz dictionary
grandizzy Apr 13, 2024
492c9f6
Merge remote-tracking branch 'origin' into scrape-result
grandizzy Apr 15, 2024
25ad08c
Perist mined values between runs
grandizzy Apr 16, 2024
d5f891d
Refactor, add persistent samples
grandizzy Apr 16, 2024
8a8f410
Apply weight to collected sample values
grandizzy Apr 17, 2024
542a2d1
Add Function to BasicTxDetails (if has outputs), to be used for decod…
grandizzy Apr 18, 2024
316b1a5
Fix clippy and fmt
grandizzy Apr 18, 2024
58fc5a9
Use prop-perturb take 1
grandizzy Apr 19, 2024
d9d8619
Decode logs using target abi, populate type samples
grandizzy Apr 21, 2024
c15bcf6
Fmt
grandizzy Apr 21, 2024
9c24971
Fix clippy, add calldetails type
grandizzy Apr 21, 2024
fd0fe4b
Fix fmt test
grandizzy Apr 21, 2024
807a95b
Insert call sample once
grandizzy Apr 22, 2024
52ced13
Merge remote-tracking branch 'origin' into scrape-result
grandizzy Apr 23, 2024
57e4df6
Merge remote-tracking branch 'origin' into scrape-result
grandizzy Apr 25, 2024
2dee260
Proper function naming
grandizzy Apr 25, 2024
d00c192
Generate state values bias using strategy
grandizzy Apr 25, 2024
c73ed83
Merge remote-tracking branch 'origin' into scrape-result
grandizzy Apr 26, 2024
55fd876
Add BasicTxDetails and CallTargetDetails struct, add Function always …
grandizzy Apr 26, 2024
09c74c3
Tests cleanup
grandizzy Apr 27, 2024
a8fb3b8
Code cleanup
grandizzy Apr 27, 2024
a90e375
Move args in CallDetails
grandizzy Apr 27, 2024
5a5ecf0
Merge remote-tracking branch 'origin' into scrape-result
grandizzy Apr 29, 2024
6c62c4e
Merge remote-tracking branch 'origin' into scrape-result
grandizzy Apr 30, 2024
59c7ef8
Fallback to old impl if we are not able to decode logs
grandizzy May 3, 2024
e5d3c97
Merge remote-tracking branch 'origin' into scrape-result
grandizzy May 6, 2024
719e462
Merge remote-tracking branch 'origin' into scrape-result
grandizzy May 14, 2024
a22471b
Refactor collect values fn
grandizzy May 14, 2024
e91dcfa
Get abi from FuzzedContracts
grandizzy May 14, 2024
875c2bc
Lookup function from identified target abi.
grandizzy May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/evm/evm/src/executors/invariant/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ impl FailedInvariantCaseData {
set_up_inner_replay(&mut executor, &self.inner_sequence);

// Replay each call from the sequence until we break the invariant.
for (sender, (addr, bytes)) in calls.iter() {
for (sender, (addr, bytes, _, _)) in calls.iter() {
let call_result =
executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?;

Expand Down Expand Up @@ -241,7 +241,7 @@ impl FailedInvariantCaseData {
}

for (seq_idx, call_index) in new_sequence.iter().enumerate() {
let (sender, (addr, bytes)) = &calls[*call_index];
let (sender, (addr, bytes, _, _)) = &calls[*call_index];

executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?;

Expand Down
2 changes: 1 addition & 1 deletion crates/evm/evm/src/executors/invariant/funcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub fn replay_run(
// set_up_inner_replay(&mut executor, &inputs);

// Replay each call from the sequence until we break the invariant.
for (sender, (addr, bytes)) in inputs.iter() {
for (sender, (addr, bytes, _, _)) in inputs.iter() {
let call_result =
executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?;

Expand Down
28 changes: 25 additions & 3 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
executors::{Executor, RawCallResult},
inspectors::Fuzzer,
};
use alloy_json_abi::{Function, JsonAbi};
use alloy_primitives::{Address, FixedBytes, U256};
use alloy_sol_types::{sol, SolCall};
use eyre::{eyre, ContextCompat, Result};
Expand Down Expand Up @@ -220,7 +221,8 @@ impl<'a> InvariantExecutor<'a> {
let mut assume_rejects_counter = 0;

while current_run < self.config.depth {
let (sender, (address, calldata)) = inputs.last().expect("no input generated");
let (sender, (address, calldata, func, abi)) =
inputs.last().expect("no input generated");

// Executes the call from the randomly generated sequence.
let call_result = if self.config.preserve_state {
Expand All @@ -247,7 +249,17 @@ impl<'a> InvariantExecutor<'a> {
let mut state_changeset =
call_result.state_changeset.to_owned().expect("no changesets");

collect_data(&mut state_changeset, sender, &call_result, &fuzz_state);
if !&call_result.reverted {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think there's any valuable data we could collect from a reverted call, hence adding this, pls let me know if I am missing something. (Further improvement when fail on revert set to false is to remove calls reverted from final sequence - should improve shrinking performance)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agreed that we don't need to collect data from a reverted call

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍 , will follow up with a PR to exclude reverted call from final sequence if running with fail-on-revert=false, should improve shrinking phase a lot

collect_data(
&mut state_changeset,
sender,
func,
abi,
&call_result,
&fuzz_state,
self.config.depth,
);
}

// Collect created contracts and add to fuzz targets only if targeted contracts
// are updatable.
Expand Down Expand Up @@ -666,8 +678,11 @@ impl<'a> InvariantExecutor<'a> {
fn collect_data(
state_changeset: &mut HashMap<Address, revm::primitives::Account>,
sender: &Address,
function: &Option<Function>,
abi: &JsonAbi,
call_result: &RawCallResult,
fuzz_state: &EvmFuzzState,
run_depth: u32,
) {
// Verify it has no code.
let mut has_code = false;
Expand All @@ -682,7 +697,14 @@ fn collect_data(
sender_changeset = state_changeset.remove(sender);
}

fuzz_state.collect_state_from_call(&call_result.logs, &*state_changeset);
fuzz_state.collect_state_from_call(
function,
abi,
&call_result.result,
&call_result.logs,
&*state_changeset,
run_depth,
);

// Re-add changes
if let Some(changed) = sender_changeset {
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/fuzz/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl Fuzzer {
!call_generator.used
{
// There's only a 30% chance that an override happens.
if let Some((sender, (contract, input))) =
if let Some((sender, (contract, input, _, _))) =
call_generator.next(call.context.caller, call.contract)
{
*call.input = input.0;
Expand Down
16 changes: 7 additions & 9 deletions crates/evm/fuzz/src/invariant/call_override.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::BasicTxDetails;
use super::{BasicTxDetails, CallDetails};
use alloy_json_abi::{Function, JsonAbi};
use alloy_primitives::{Address, Bytes};
use parking_lot::{Mutex, RwLock};
use proptest::{
Expand All @@ -17,7 +18,7 @@ pub struct RandomCallGenerator {
/// Runner that will generate the call from the strategy.
pub runner: Arc<Mutex<TestRunner>>,
/// Strategy to be used to generate calls from `target_reference`.
pub strategy: SBoxedStrategy<Option<(Address, Bytes)>>,
pub strategy: SBoxedStrategy<Option<CallDetails>>,
/// Reference to which contract we want a fuzzed calldata from.
pub target_reference: Arc<RwLock<Address>>,
/// Flag to know if a call has been overridden. Don't allow nesting for now.
Expand All @@ -33,7 +34,7 @@ impl RandomCallGenerator {
pub fn new(
test_address: Address,
runner: TestRunner,
strategy: SBoxedStrategy<(Address, Bytes)>,
strategy: SBoxedStrategy<(Address, Bytes, Option<Function>, JsonAbi)>,
target_reference: Arc<RwLock<Address>>,
) -> Self {
let strategy = weighted(0.9, strategy).sboxed();
Expand Down Expand Up @@ -77,12 +78,9 @@ impl RandomCallGenerator {
*self.target_reference.write() = original_caller;

// `original_caller` has a 80% chance of being the `new_target`.
let choice = self
.strategy
.new_tree(&mut self.runner.lock())
.unwrap()
.current()
.map(|(new_target, calldata)| (new_caller, (new_target, calldata)));
let choice = self.strategy.new_tree(&mut self.runner.lock()).unwrap().current().map(
|(new_target, calldata, func, abi)| (new_caller, (new_target, calldata, func, abi)),
);

self.last_sequence.write().push(choice.clone());
choice
Expand Down
10 changes: 8 additions & 2 deletions crates/evm/fuzz/src/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ impl FuzzRunIdentifiedContracts {
}
}

/// (Sender, (TargetContract, Calldata))
pub type BasicTxDetails = (Address, (Address, Bytes));
/// (Sender, CallDetails)
/// TODO: replace type with struct
pub type BasicTxDetails = (Address, CallDetails);

/// (TargetContractAddress, Calldata, Function, TargetContractAbi)
/// Function and contract abi are used for collecting sample values from result and logs.
/// Function is set for calls that returns values.
pub type CallDetails = (Address, Bytes, Option<Function>, JsonAbi);

/// Test contract which is testing its invariants.
#[derive(Clone, Debug)]
Expand Down
39 changes: 28 additions & 11 deletions crates/evm/fuzz/src/strategies/invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub fn override_call_strat(
contracts: FuzzRunIdentifiedContracts,
target: Arc<RwLock<Address>>,
calldata_fuzz_config: CalldataFuzzDictionary,
) -> SBoxedStrategy<(Address, Bytes)> {
) -> SBoxedStrategy<(Address, Bytes, Option<Function>, JsonAbi)> {
let contracts_ref = contracts.targets.clone();
proptest::prop_oneof![
80 => proptest::strategy::LazyJust::new(move || *target.read()),
Expand All @@ -26,7 +26,7 @@ pub fn override_call_strat(
let fuzz_state = fuzz_state.clone();
let calldata_fuzz_config = calldata_fuzz_config.clone();

let func = {
let (func, abi) = {
let contracts = contracts.targets.lock();
let (_, abi, functions) = contracts.get(&target_address).unwrap_or_else(|| {
// Choose a random contract if target selected by lazy strategy is not in fuzz run
Expand All @@ -36,11 +36,17 @@ pub fn override_call_strat(
let (_, contract_specs) = contracts.iter().nth(rand_index).unwrap();
contract_specs
});
select_random_function(abi, functions)
(select_random_function(abi, functions), abi.clone())
};

func.prop_flat_map(move |func| {
fuzz_contract_with_calldata(&fuzz_state, &calldata_fuzz_config, target_address, func)
fuzz_contract_with_calldata(
&fuzz_state,
&calldata_fuzz_config,
target_address,
func,
abi.clone(),
)
})
})
.sboxed()
Expand Down Expand Up @@ -80,23 +86,28 @@ fn generate_call(
let senders = Rc::new(senders);
any::<prop::sample::Selector>()
.prop_flat_map(move |selector| {
let (contract, func) = {
let (contract, func, abi) = {
let contracts = contracts.targets.lock();
let contracts =
contracts.iter().filter(|(_, (_, abi, _))| !abi.functions.is_empty());
let (&contract, (_, abi, functions)) = selector.select(contracts);

let func = select_random_function(abi, functions);
(contract, func)
(contract, func, abi.clone())
};

let senders = senders.clone();
let fuzz_state = fuzz_state.clone();
let calldata_fuzz_config = calldata_fuzz_config.clone();
func.prop_flat_map(move |func| {
let sender = select_random_sender(&fuzz_state, senders.clone(), dictionary_weight);
let contract =
fuzz_contract_with_calldata(&fuzz_state, &calldata_fuzz_config, contract, func);
let contract = fuzz_contract_with_calldata(
&fuzz_state,
&calldata_fuzz_config,
contract,
func,
abi.clone(),
);
(sender, contract)
})
})
Expand Down Expand Up @@ -167,17 +178,23 @@ pub fn fuzz_contract_with_calldata(
calldata_fuzz_config: &CalldataFuzzDictionary,
contract: Address,
func: Function,
) -> impl Strategy<Value = (Address, Bytes)> {
contract_abi: JsonAbi,
) -> impl Strategy<Value = (Address, Bytes, Option<Function>, JsonAbi)> {
// We need to compose all the strategies generated for each parameter in all possible
// combinations.
// `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
#[allow(clippy::arc_with_non_send_sync)]
prop_oneof![
60 => fuzz_calldata_with_config(func.clone(), Some(calldata_fuzz_config)),
40 => fuzz_calldata_from_state(func, fuzz_state),
40 => fuzz_calldata_from_state(func.clone(), fuzz_state),
]
.prop_map(move |calldata| {
trace!(input=?calldata);
(contract, calldata)
if !func.outputs.is_empty() {
// If function has outputs then return it for decoding result.
(contract, calldata, Some(func.clone()), contract_abi.clone())
} else {
(contract, calldata, None, contract_abi.clone())
}
})
}
13 changes: 12 additions & 1 deletion crates/evm/fuzz/src/strategies/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,21 @@ pub fn fuzz_param_from_state(
// Value strategy that uses the state.
let value = || {
let state = state.clone();
let param = param.clone();
// Use `Index` instead of `Selector` to not iterate over the entire dictionary.
any::<prop::sample::Index>().prop_map(move |index| {
let state = state.dictionary_read();
let values = state.values();
let bias = rand::thread_rng().gen_range(0..100);
let values = match bias {
x if x < 50 => {
if let Some(sample_values) = state.samples(param.clone()) {
sample_values
} else {
state.values()
}
}
_ => state.values(),
};
let index = index.index(values.len());
*values.iter().nth(index).unwrap()
})
Expand Down