diff --git a/cli/src/cmd/verify.rs b/cli/src/cmd/verify.rs index 0430492479c13..f38d582d6a188 100644 --- a/cli/src/cmd/verify.rs +++ b/cli/src/cmd/verify.rs @@ -78,6 +78,7 @@ pub async fn run_verify(args: &VerifyArgs) -> eyre::Result<()> { force: false, hardhat, libraries: vec![], + watch: Default::default(), }; let project = build_args.project()?; diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 96fd057d992a8..e6e97ceba53d3 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -16,7 +16,7 @@ use proptest::{ }; use serde::{Deserialize, Serialize}; -mod strategies; +pub(crate) mod strategies; /// Wrapper around any [`Evm`](crate::Evm) implementor which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/). /// diff --git a/evm-adapters/src/invariant_fuzz.rs b/evm-adapters/src/invariant_fuzz.rs new file mode 100644 index 0000000000000..528911bce1c62 --- /dev/null +++ b/evm-adapters/src/invariant_fuzz.rs @@ -0,0 +1,474 @@ +//! Fuzzing support abstracted over the [`Evm`](crate::Evm) used +use crate::{fuzz::*, Evm}; +use std::collections::BTreeMap; + +use ethers::{ + abi::{Abi, Function, ParamType, Token, Tokenizable}, + types::{Address, Bytes, I256, U256}, +}; +use std::{ + cell::{RefCell, RefMut}, + marker::PhantomData, +}; + +pub use proptest::test_runner::Config as FuzzConfig; +use proptest::{ + prelude::*, + test_runner::{TestError, TestRunner}, +}; +use serde::{Deserialize, Serialize}; + +use crate::fuzz::strategies; + +/// Wrapper around any [`Evm`](crate::Evm) implementor which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/). +/// +/// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with +/// inputs, until it finds a counterexample. The provided `TestRunner` contains all the +/// configuration which can be overridden via [environment variables](https://docs.rs/proptest/1.0.0/proptest/test_runner/struct.Config.html) +#[derive(Debug)] +pub struct InvariantExecutor<'a, E, S> { + evm: RefCell<&'a mut E>, + runner: TestRunner, + state: PhantomData, + sender: Address, + contracts: &'a BTreeMap, +} + +impl<'a, S, E: Evm> InvariantExecutor<'a, E, S> { + pub fn into_inner(self) -> &'a mut E { + self.evm.into_inner() + } + + /// Returns a mutable reference to the fuzzer's internal EVM instance + pub fn as_mut(&self) -> RefMut<'_, &'a mut E> { + self.evm.borrow_mut() + } + + /// Instantiates a fuzzed executor EVM given a testrunner + pub fn new( + evm: &'a mut E, + runner: TestRunner, + sender: Address, + contracts: &'a BTreeMap, + ) -> Self { + Self { evm: RefCell::new(evm), runner, state: PhantomData, sender, contracts } + } + + /// Fuzzes the provided function, assuming it is available at the contract at `address` + /// If `should_fail` is set to `true`, then it will stop only when there's a success + /// test case. + /// + /// Returns a list of all the consumed gas and calldata of every fuzz case + pub fn invariant_fuzz( + &self, + invariant_address: Address, + abi: Option<&Abi>, + ) -> Option> + where + // We need to be able to clone the state so as to snapshot it and reset + // it back after every test run, to have isolation of state across each + // fuzz test run. + S: Clone, + { + let invariants: Vec; + if let Some(abi) = abi { + invariants = + abi.functions().filter(|func| func.name.starts_with("invariant")).cloned().collect() + } else { + return None + }; + + let contracts: BTreeMap = self + .contracts + .clone() + .into_iter() + .filter(|(addr, _)| { + *addr != + Address::from_slice( + &hex::decode("7109709ECfa91a80626fF3989D68f67F5b1DD12D").unwrap(), + ) && + *addr != + Address::from_slice( + &hex::decode("000000000000000000636F6e736F6c652e6c6f67").unwrap(), + ) + }) + .collect(); + let strat = invariant_strat(15, contracts); + + // Snapshot the state before the test starts running + let pre_test_state = self.evm.borrow().state().clone(); + + // stores the consumed gas and calldata of every successful fuzz call + let fuzz_cases: RefCell> = RefCell::new(Default::default()); + + // stores the latest reason of a test call, this will hold the return reason of failed test + // case if the runner failed + let return_reason: RefCell> = RefCell::new(None); + let revert_reason = RefCell::new(None); + let mut all_invars = BTreeMap::new(); + invariants.iter().for_each(|f| { + all_invars.insert(f.name.to_string(), None); + }); + let invariant_doesnt_hold = RefCell::new(all_invars); + + let mut runner = self.runner.clone(); + let _test_error = runner + .run(&strat, |inputs| { + let mut evm = self.evm.borrow_mut(); + // Before each test, we must reset to the initial state + evm.reset(pre_test_state.clone()); + + // println!("inputs len: {:?}", inputs.len()); + 'all: for (address, calldata) in inputs.iter() { + // println!("address {:?} {:?}", address, hex::encode(&calldata)); + let (_, reason, gas, _) = evm + .call_raw(self.sender, *address, calldata.clone(), 0.into(), false) + .expect("could not make raw evm call"); + + if !is_fail(*evm, &reason) { + // iterate over invariants, making sure they dont fail + for func in invariants.iter() { + let (retdata, status, _gas, _logs) = evm + .call_unchecked(self.sender, invariant_address, &func, (), 0.into()) + .expect("EVM error"); + if is_fail(*evm, &status) { + invariant_doesnt_hold.borrow_mut().insert( + func.name.clone(), + Some(InvariantFuzzError { + test_error: proptest::test_runner::TestError::Fail( + format!( + "{}, reason: '{}'", + func.name, + match foundry_utils::decode_revert( + retdata.as_ref(), + abi + ) { + Ok(e) => e, + Err(e) => e.to_string(), + } + ) + .into(), + inputs.clone(), + ), + return_reason: status, + revert_reason: foundry_utils::decode_revert( + retdata.as_ref(), + abi, + ) + .unwrap_or_default(), + addr: invariant_address, + func: func.short_signature().into(), + }), + ); + break 'all + } else { + // This will panic and get caught by the executor + if !evm.check_success(invariant_address, &reason, false) { + invariant_doesnt_hold.borrow_mut().insert( + func.name.clone(), + Some(InvariantFuzzError { + test_error: proptest::test_runner::TestError::Fail( + format!( + "{}, reason: '{}'", + func.name, + match foundry_utils::decode_revert( + retdata.as_ref(), + abi + ) { + Ok(e) => e, + Err(e) => e.to_string(), + } + ) + .into(), + inputs.clone(), + ), + return_reason: status, + revert_reason: foundry_utils::decode_revert( + retdata.as_ref(), + abi, + ) + .unwrap_or_default(), + addr: invariant_address, + func: func.short_signature().into(), + }), + ); + break 'all + } + } + } + // push test case to the case set + fuzz_cases.borrow_mut().push(FuzzCase { calldata: calldata.clone(), gas }); + } else { + // call failed, continue on + } + } + + Ok(()) + }) + .err() + .map(|test_error| InvariantFuzzError { + test_error, + return_reason: return_reason.into_inner().expect("Reason must be set"), + revert_reason: revert_reason.into_inner().expect("Revert error string must be set"), + addr: invariant_address, + func: ethers::prelude::Bytes::default(), + }); + + self.evm.borrow_mut().reset(pre_test_state.clone()); + + Some(InvariantFuzzTestResult { + invariants: invariant_doesnt_hold.into_inner(), + cases: FuzzedCases::new(fuzz_cases.into_inner()), + }) + } +} + +/// The outcome of a fuzz test +pub struct InvariantFuzzTestResult { + pub invariants: BTreeMap>>, + /// Every successful fuzz test case + pub cases: FuzzedCases, +} + +impl InvariantFuzzTestResult { + /// Returns `true` if all test cases succeeded + pub fn is_ok(&self) -> bool { + !self.invariants.iter().any(|(_k, i)| i.is_some()) + // self.test_error.is_none() + } + + /// Returns `true` if a test case failed + pub fn is_err(&self) -> bool { + self.invariants.iter().any(|(_k, i)| i.is_some()) + } +} + +pub struct InvariantFuzzError { + /// The proptest error occurred as a result of a test case + pub test_error: TestError>, + /// The return reason of the offending call + pub return_reason: Reason, + /// The revert string of the offending call + pub revert_reason: String, + /// Address of the invariant asserter + pub addr: Address, + /// Function data for invariant check + pub func: ethers::prelude::Bytes, +} + +fn is_fail + crate::Evm, T>( + _evm: &mut E, + status: &T, +) -> bool { + >::is_fail(status) +} + +/// Container type for all successful test cases +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InvariantFuzzedCases { + cases: Vec, +} + +impl InvariantFuzzedCases { + pub fn new(mut cases: Vec) -> Self { + cases.sort_by_key(|c| c.gas); + Self { cases } + } + + pub fn cases(&self) -> &[FuzzCase] { + &self.cases + } + + pub fn into_cases(self) -> Vec { + self.cases + } + + /// Returns the median gas of all test cases + pub fn median_gas(&self) -> u64 { + let mid = self.cases.len() / 2; + self.cases.get(mid).map(|c| c.gas).unwrap_or_default() + } + + /// Returns the average gas use of all test cases + pub fn mean_gas(&self) -> u64 { + if self.cases.is_empty() { + return 0 + } + + (self.cases.iter().map(|c| c.gas as u128).sum::() / self.cases.len() as u128) as u64 + } + + pub fn highest(&self) -> Option<&FuzzCase> { + self.cases.last() + } + + pub fn lowest(&self) -> Option<&FuzzCase> { + self.cases.first() + } + + pub fn highest_gas(&self) -> u64 { + self.highest().map(|c| c.gas).unwrap_or_default() + } + + pub fn lowest_gas(&self) -> u64 { + self.lowest().map(|c| c.gas).unwrap_or_default() + } +} + +/// Data of a single fuzz test case +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InvariantFuzzCase { + /// The calldata used for this fuzz test + pub calldata: Bytes, + // Consumed gas + pub gas: u64, +} + +pub fn invariant_strat( + depth: usize, + contracts: BTreeMap, +) -> BoxedStrategy> { + let iters = 1..depth + 1; + proptest::collection::vec(gen_call(contracts), iters).boxed() +} + +fn gen_call(contracts: BTreeMap) -> BoxedStrategy<(Address, Bytes)> { + let random_contract = select_random_contract(contracts); + random_contract + .prop_flat_map(move |(contract, abi)| { + let func = select_random_function(abi); + func.prop_flat_map(move |func| fuzz_calldata(contract, func.clone())) + }) + .boxed() +} + +fn select_random_contract( + contracts: BTreeMap, +) -> impl Strategy { + let selectors = any::(); + selectors.prop_map(move |selector| { + let res = selector.select(&contracts); + (*res.0, res.1 .1.clone()) + }) +} + +fn select_random_function(abi: Abi) -> impl Strategy { + let selectors = any::(); + let possible_funcs: Vec = abi + .functions() + .filter(|func| { + !matches!( + func.state_mutability, + ethers::abi::StateMutability::Pure | ethers::abi::StateMutability::View + ) + }) + .cloned() + .collect(); + selectors.prop_map(move |selector| { + let func = selector.select(&possible_funcs); + func.clone() + }) +} + +/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata +/// for that function's input types. +pub fn fuzz_calldata(addr: Address, func: Function) -> impl Strategy { + // We need to compose all the strategies generated for each parameter in all + // possible combinations + let strats = func.inputs.iter().map(|input| fuzz_param(&input.kind)).collect::>(); + + strats.prop_map(move |tokens| { + tracing::trace!(input = ?tokens); + (addr, func.encode_input(&tokens).unwrap().into()) + }) +} + +/// The max length of arrays we fuzz for is 256. +const MAX_ARRAY_LEN: usize = 256; + +/// Given an ethabi parameter type, returns a proptest strategy for generating values for that +/// datatype. Works with ABI Encoder v2 tuples. +fn fuzz_param(param: &ParamType) -> impl Strategy { + match param { + ParamType::Address => { + // The key to making this work is the `boxed()` call which type erases everything + // https://altsysrq.github.io/proptest-book/proptest/tutorial/transforming-strategies.html + any::<[u8; 20]>().prop_map(|x| Address::from_slice(&x).into_token()).boxed() + } + ParamType::Bytes => any::>().prop_map(|x| Bytes::from(x).into_token()).boxed(), + // For ints and uints we sample from a U256, then wrap it to the correct size with a + // modulo operation. Note that this introduces modulo bias, but it can be removed with + // rejection sampling if it's determined the bias is too severe. Rejection sampling may + // slow down tests as it resamples bad values, so may want to benchmark the performance + // hit and weigh that against the current bias before implementing + ParamType::Int(n) => match n / 8 { + 32 => any::<[u8; 32]>() + .prop_map(move |x| I256::from_raw(U256::from(&x)).into_token()) + .boxed(), + y @ 1..=31 => any::<[u8; 32]>() + .prop_map(move |x| { + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(&x) % U256::from(2).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => { + strategies::UintStrategy::new(*n, vec![]).prop_map(|x| x.into_token()).boxed() + } + ParamType::Bool => any::().prop_map(|x| x.into_token()).boxed(), + ParamType::String => any::>() + .prop_map(|x| Token::String(unsafe { std::str::from_utf8_unchecked(&x).to_string() })) + .boxed(), + ParamType::Array(param) => proptest::collection::vec(fuzz_param(param), 0..MAX_ARRAY_LEN) + .prop_map(Token::Array) + .boxed(), + ParamType::FixedBytes(size) => (0..*size as u64) + .map(|_| any::()) + .collect::>() + .prop_map(Token::FixedBytes) + .boxed(), + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| fuzz_param(param).prop_map(|param| param.into_token())) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => { + params.iter().map(fuzz_param).collect::>().prop_map(Token::Tuple).boxed() + } + } +} + +#[cfg(test)] +#[cfg(feature = "sputnik")] +mod tests { + use super::*; + + use crate::{ + sputnik::helpers::{fuzzvm, vm}, + test_helpers::COMPILED, + Evm, + }; + + #[test] + fn prints_fuzzed_revert_reasons() { + let mut evm = vm(); + + let compiled = COMPILED.find("FuzzTests").expect("could not find contract"); + let (addr, _, _, _) = + evm.deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()).unwrap(); + + let evm = fuzzvm(&mut evm); + + let func = compiled.abi.unwrap().function("testFuzzedRevert").unwrap(); + let res = evm.fuzz(func, addr, false, compiled.abi); + let error = res.test_error.unwrap(); + let revert_reason = error.revert_reason; + assert_eq!(revert_reason, "fuzztest-revert"); + } +} diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index 6607606fb083d..749e66863eee5 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -12,6 +12,8 @@ pub use blocking_provider::BlockingProvider; pub mod fuzz; +pub mod invariant_fuzz; + pub mod call_tracing; pub mod gas_report; @@ -56,7 +58,7 @@ pub enum EvmError { /// only needs to specify the transaction parameters pub trait Evm { /// The returned reason type from an EVM (Success / Revert/ Stopped etc.) - type ReturnReason: std::fmt::Debug + PartialEq; + type ReturnReason: std::fmt::Debug + PartialEq + Clone; /// Gets the revert reason type fn revert() -> Self::ReturnReason; diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index 596a3088a0df6..faba42d2c130d 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -103,7 +103,9 @@ impl MultiContractRunnerBuilder { let abi = contract.abi.expect("We should have an abi by now"); // if its a test, add it to deployable contracts if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && - abi.functions().any(|func| func.name.starts_with("test")) + abi.functions().any(|func| { + func.name.starts_with("test") || func.name.starts_with("invariant") + }) { deployable_contracts .insert(fname.clone(), (abi.clone(), bytecode, dependencies.to_vec())); diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 80d508b701f14..afa69882b33d2 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -1,10 +1,12 @@ use crate::TestFilter; use evm_adapters::{ evm_opts::EvmOpts, + invariant_fuzz::{InvariantExecutor, InvariantFuzzTestResult}, sputnik::{helpers::TestSputnikVM, Executor, PRECOMPILES_MAP}, }; use rayon::iter::ParallelIterator; use sputnik::{backend::Backend, Config}; +use std::cell::RefCell; use ethers::{ abi::{Abi, Event, Function, Token}, @@ -25,6 +27,9 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CounterExample { + /// Address to which to call to + pub addr: Option
, + /// The data to provide pub calldata: Bytes, // Token does not implement Serde (lol), so we just serialize the calldata #[serde(skip)] @@ -34,7 +39,17 @@ pub struct CounterExample { impl fmt::Display for CounterExample { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let args = foundry_utils::format_tokens(&self.args).collect::>().join(", "); - write!(f, "calldata=0x{}, args=[{}]", hex::encode(&self.calldata), args) + if let Some(addr) = self.addr { + write!( + f, + "addr={:?}, calldata=0x{}, args=[{}]", + addr, + hex::encode(&self.calldata), + args + ) + } else { + write!(f, "calldata=0x{}, args=[{}]", hex::encode(&self.calldata), args) + } } } @@ -127,6 +142,8 @@ pub enum TestKind { Standard(u64), /// A solidity fuzz test, that stores all test cases Fuzz(FuzzedCases), + /// Invariant + Invariant(String, FuzzedCases), } impl TestKind { @@ -139,6 +156,11 @@ impl TestKind { median: fuzzed.median_gas(), mean: fuzzed.mean_gas(), }, + TestKind::Invariant(_s, fuzzed) => TestKindGas::Fuzz { + runs: fuzzed.cases().len(), + median: fuzzed.median_gas(), + mean: fuzzed.mean_gas(), + }, } } } @@ -201,7 +223,10 @@ impl<'a, B: Backend> ContractRunner<'a, B> { impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { /// Creates a new EVM and deploys the test contract inside the runner /// from the sending account. - pub fn new_sputnik_evm(&'a self) -> eyre::Result<(Address, TestSputnikVM<'a, B>, Vec)> { + pub fn new_sputnik_evm( + &'a self, + force_tracing: bool, + ) -> eyre::Result<(Address, TestSputnikVM<'a, B>, Vec)> { // create the EVM, clone the backend. let mut executor = Executor::new_with_cheatcodes( self.backend.clone(), @@ -209,7 +234,7 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { self.evm_cfg, &*PRECOMPILES_MAP, self.evm_opts.ffi, - self.evm_opts.verbosity > 2, + self.evm_opts.verbosity > 2 || force_tracing, self.evm_opts.debug, ); @@ -243,6 +268,9 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { .filter(|func| func.name.starts_with("test")) .filter(|func| filter.matches_test(&func.name)) .collect::>(); + println!("{:?}", test_fns); + let has_invar_fns = + self.contract.functions().into_iter().any(|func| func.name.starts_with("invariant")); // run all unit tests let unit_tests = test_fns @@ -254,7 +282,7 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { }) .collect::>>()?; - let map = if let Some(fuzzer) = fuzzer { + let mut map = if let Some(ref fuzzer) = fuzzer { let fuzz_tests = test_fns .par_iter() .filter(|func| !func.inputs.is_empty()) @@ -272,6 +300,25 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { unit_tests }; + println!("has invar {:?}", has_invar_fns); + let map = if has_invar_fns { + if let Some(fuzzer) = fuzzer { + let results = + self.run_invariant_test(needs_setup, fuzzer.clone(), known_contracts)?; + results.into_iter().for_each(|result| match result.kind { + TestKind::Invariant(ref name, ref _cases) => { + map.insert(name.to_string(), result); + } + _ => unreachable!(), + }); + map + } else { + map + } + } else { + map + }; + if !map.is_empty() { let successful = map.iter().filter(|(_, tst)| tst.success).count(); let duration = Instant::now().duration_since(start); @@ -295,7 +342,7 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "unit-testing"); - let (address, mut evm, init_logs) = self.new_sputnik_evm()?; + let (address, mut evm, init_logs) = self.new_sputnik_evm(false)?; let errors_abi = self.execution_info.as_ref().map(|(_, _, errors)| errors); let errors_abi = if let Some(ref abi) = errors_abi { abi } else { self.contract }; @@ -399,6 +446,231 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { }) } + #[tracing::instrument(name = "invariant-test", skip_all)] + pub fn run_invariant_test( + &self, + setup: bool, + runner: TestRunner, + known_contracts: Option<&BTreeMap)>>, + ) -> Result> { + println!("running invariant tests for contract"); + // do not trace in fuzztests, as it's a big performance hit + let start = Instant::now(); + + let (address, mut evm, init_logs) = self.new_sputnik_evm(true)?; + + let mut traces: Vec = Vec::new(); + let identified_contracts: RefCell> = + RefCell::new(BTreeMap::new()); + + for trace in evm.traces().iter() { + trace.update_identified( + 0, + known_contracts.expect("traces enabled but no identified_contracts"), + &mut identified_contracts.borrow_mut(), + &evm, + ); + } + + // clear out the deployment trace + evm.reset_traces(); + + // call the setup function in each test to reset the test's state. + if setup { + tracing::trace!("setting up"); + match evm.setup(address) { + Ok((_reason, _setup_logs)) => {} + Err(e) => { + // if tracing is enabled, just return it as a failed test + // otherwise abort + if evm.tracing_enabled() { + self.update_traces_ref( + &mut traces, + &mut identified_contracts.borrow_mut(), + known_contracts, + setup, + false, + &mut evm, + ); + } + return Ok(vec![TestResult { + success: false, + reason: Some("Setup failed: ".to_string() + &e.to_string()), + gas_used: 0, + counterexample: None, + logs: vec![], + kind: TestKind::Fuzz(FuzzedCases::new(vec![])), + traces: Some(traces), + identified_contracts: Some(identified_contracts.into_inner()), + debug_calls: if evm.state().debug_enabled { + Some(evm.debug_calls()) + } else { + None + }, + labeled_addresses: evm.state().labels.clone(), + }]) + } + } + } + + self.update_traces_ref( + &mut traces, + &mut identified_contracts.borrow_mut(), + known_contracts, + true, + false, + &mut evm, + ); + + let mut logs = init_logs; + + let prev = evm.set_tracing_enabled(false); + + // instantiate the fuzzed evm in line + let ident = identified_contracts.clone(); + let ident = ident.borrow(); + let evm = InvariantExecutor::new(&mut evm, runner, self.sender, &ident); + if let Some(InvariantFuzzTestResult { invariants, cases }) = + evm.invariant_fuzz(address, Some(self.contract)) + { + let evm = evm.into_inner(); + + let _duration = Instant::now().duration_since(start); + // tracing::debug!(?duration, %success); + + let results = invariants + .iter() + .map(|(k, test_error)| { + if let Some(ref error) = test_error { + // we want traces for a failed fuzz + if let TestError::Fail(_reason, vec_addr_bytes) = &error.test_error { + if prev { + let _ = evm.set_tracing_enabled(true); + } + for (addr, bytes) in vec_addr_bytes.iter() { + println!("rerunning fails {:?} {:?}", addr, hex::encode(bytes)); + let (_retdata, status, _gas, execution_logs) = evm + .call_raw(self.sender, *addr, bytes.clone(), 0.into(), false) + .expect("bad call to evm"); + + if is_fail(evm, status) { + logs.extend(execution_logs); + // add reverted logs + logs.extend(evm.all_logs()); + } else { + logs.extend(execution_logs); + } + self.update_traces_ref( + &mut traces, + &mut identified_contracts.borrow_mut(), + known_contracts, + false, + true, + evm, + ); + + let (_retdata, status, _gas, execution_logs) = evm + .call_raw( + self.sender, + error.addr, + error.func.clone(), + 0.into(), + false, + ) + .expect("bad call to evm"); + if is_fail(evm, status) { + logs.extend(execution_logs); + // add reverted logs + logs.extend(evm.all_logs()); + self.update_traces_ref( + &mut traces, + &mut identified_contracts.borrow_mut(), + known_contracts, + false, + true, + evm, + ); + break + } else { + logs.extend(execution_logs); + self.update_traces_ref( + &mut traces, + &mut identified_contracts.borrow_mut(), + known_contracts, + false, + true, + evm, + ); + } + } + } + } + + let success = test_error.is_none(); + let mut counterexample = None; + let mut reason = None; + if let Some(err) = test_error { + match &err.test_error { + TestError::Fail(_, vec_addr_bytes) => { + let addr = vec_addr_bytes[0].0; + let value = &vec_addr_bytes[0].1; + let ident = identified_contracts.borrow(); + let abi = + &ident.get(&addr).expect("Couldnt call unknown contract").1; + let func = abi + .functions() + .find(|f| f.short_signature() == value.as_ref()[0..4]) + .expect("Couldnt find function"); + // skip the function selector when decoding + let args = func + .decode_input(&value.as_ref()[4..]) + .expect("Unable to decode input"); + let counter = CounterExample { + addr: Some(addr), + calldata: value.clone(), + args, + }; + counterexample = Some(counter); + tracing::info!( + "Found minimal failing case: {}", + hex::encode(&value) + ); + } + result => panic!("Unexpected test result: {:?}", result), + } + if !err.revert_reason.is_empty() { + reason = Some(err.revert_reason.clone()); + } + } + + let duration = Instant::now().duration_since(start); + tracing::debug!(?duration, %success); + + // from that call? + TestResult { + success, + reason, + gas_used: cases.median_gas(), + counterexample, + logs: logs.clone(), + kind: TestKind::Invariant(k.to_string(), cases.clone()), + traces: Some(traces.clone()), + identified_contracts: Some(identified_contracts.clone().into_inner()), + debug_calls: if evm.state().debug_enabled { + Some(evm.debug_calls()) + } else { + None + }, + labeled_addresses: evm.state().labels.clone(), + } + }) + .collect(); + Ok(results) + } else { + Ok(vec![]) + } + } + #[tracing::instrument(name = "fuzz-test", skip_all, fields(name = %func.signature()))] pub fn run_fuzz_test( &self, @@ -412,7 +684,7 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "fuzzing"); - let (address, mut evm, init_logs) = self.new_sputnik_evm()?; + let (address, mut evm, init_logs) = self.new_sputnik_evm(false)?; let mut traces: Option> = None; let mut identified_contracts: Option> = None; @@ -503,7 +775,7 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { TestError::Fail(_, value) => { // skip the function selector when decoding let args = func.decode_input(&value.as_ref()[4..])?; - let counter = CounterExample { calldata: value.clone(), args }; + let counter = CounterExample { addr: None, calldata: value.clone(), args }; counterexample = Some(counter); tracing::info!("Found minimal failing case: {}", hex::encode(&value)); } @@ -574,6 +846,51 @@ impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { } evm.reset_traces(); } + + fn update_traces_ref>( + &self, + traces: &mut Vec, + identified_contracts: &mut BTreeMap, + known_contracts: Option<&BTreeMap)>>, + setup: bool, + skip: bool, + evm: &mut E, + ) { + let evm_traces = evm.traces(); + if !evm_traces.is_empty() && evm.tracing_enabled() { + let mut ident = identified_contracts.clone(); + // create an iter over the traces + let mut trace_iter = evm_traces.into_iter(); + if setup { + // grab the setup trace if it exists + let setup = trace_iter.next().expect("no setup trace"); + setup.update_identified( + 0, + known_contracts.expect("traces enabled but no identified_contracts"), + &mut ident, + evm, + ); + traces.push(setup); + } + // grab the test trace + while let Some(test_trace) = trace_iter.next() { + test_trace.update_identified( + 0, + known_contracts.expect("traces enabled but no identified_contracts"), + &mut ident, + evm, + ); + + if test_trace.arena[0].trace.addr != Address::zero() { + traces.push(test_trace); + } + } + + // pass back the identified contracts and traces + *identified_contracts = ident; + } + evm.reset_traces(); + } } // Helper functions for getting the revert status for a `ReturnReason` without having