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 (#5868) - configure calldata fuzzed addresses dictionary #7240

Merged
merged 7 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/config/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ pub struct FuzzDictionaryConfig {
/// Once the fuzzer exceeds this limit, it will start evicting random entries
#[serde(deserialize_with = "crate::deserialize_usize_or_max")]
pub max_fuzz_dictionary_values: usize,
/// How many random addresses to use and to recycle when fuzzing calldata.
/// If not specified then `max_fuzz_dictionary_addresses` value applies.
#[serde(deserialize_with = "crate::deserialize_usize_or_max")]
pub max_calldata_fuzz_dictionary_addresses: usize,
}

impl Default for FuzzDictionaryConfig {
Expand All @@ -98,6 +102,7 @@ impl Default for FuzzDictionaryConfig {
max_fuzz_dictionary_addresses: (300 * 1024 * 1024) / 20,
// limit this to 200MB
max_fuzz_dictionary_values: (200 * 1024 * 1024) / 32,
max_calldata_fuzz_dictionary_addresses: 0,
}
}
}
Expand Down
20 changes: 16 additions & 4 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@ use std::{cell::RefCell, collections::BTreeMap, sync::Arc};

mod error;
pub use error::{InvariantFailures, InvariantFuzzError, InvariantFuzzTestResult};
use foundry_evm_fuzz::strategies::CalldataFuzzDictionary;

mod funcs;
pub use funcs::{assert_invariants, replay_run};

/// Alias for (Dictionary for fuzzing, initial contracts to fuzz and an InvariantStrategy).
type InvariantPreparation =
(EvmFuzzState, FuzzRunIdentifiedContracts, BoxedStrategy<Vec<BasicTxDetails>>);
type InvariantPreparation = (
EvmFuzzState,
FuzzRunIdentifiedContracts,
BoxedStrategy<Vec<BasicTxDetails>>,
CalldataFuzzDictionary,
);

/// Enriched results of an invariant run check.
///
Expand Down Expand Up @@ -104,7 +109,8 @@ impl<'a> InvariantExecutor<'a> {
return Err(eyre!("Invariant test function should have no inputs"))
}

let (fuzz_state, targeted_contracts, strat) = self.prepare_fuzzing(&invariant_contract)?;
let (fuzz_state, targeted_contracts, strat, calldata_fuzz_dictionary) =
self.prepare_fuzzing(&invariant_contract)?;

// Stores the consumed gas and calldata of every successful fuzz call.
let fuzz_cases: RefCell<Vec<FuzzedCases>> = RefCell::new(Default::default());
Expand Down Expand Up @@ -245,6 +251,7 @@ impl<'a> InvariantExecutor<'a> {
Ok(())
});

trace!(target: "forge::test::invariant::calldata_address_fuzz_dictionary", "{:?}", calldata_fuzz_dictionary.inner.addresses);
trace!(target: "forge::test::invariant::dictionary", "{:?}", fuzz_state.read().values().iter().map(hex::encode).collect::<Vec<_>>());

let (reverts, error) = failures.into_inner().into_inner();
Expand Down Expand Up @@ -283,12 +290,16 @@ impl<'a> InvariantExecutor<'a> {
let targeted_contracts: FuzzRunIdentifiedContracts =
Arc::new(Mutex::new(targeted_contracts));

let calldata_fuzz_config =
CalldataFuzzDictionary::new(&self.config.dictionary, fuzz_state.clone());

// Creates the invariant strategy.
let strat = invariant_strat(
fuzz_state.clone(),
targeted_senders,
targeted_contracts.clone(),
self.config.dictionary.dictionary_weight,
calldata_fuzz_config.clone(),
)
.no_shrink()
.boxed();
Expand All @@ -306,6 +317,7 @@ impl<'a> InvariantExecutor<'a> {
fuzz_state.clone(),
targeted_contracts.clone(),
target_contract_ref.clone(),
calldata_fuzz_config.clone(),
),
target_contract_ref,
));
Expand All @@ -314,7 +326,7 @@ impl<'a> InvariantExecutor<'a> {
self.executor.inspector.fuzzer =
Some(Fuzzer { call_generator, fuzz_state: fuzz_state.clone(), collect: true });

Ok((fuzz_state, targeted_contracts, strat))
Ok((fuzz_state, targeted_contracts, strat, calldata_fuzz_config))
}

/// Fills the `InvariantExecutor` with the artifact identifier filters (in `path:name` string
Expand Down
73 changes: 70 additions & 3 deletions crates/evm/fuzz/src/strategies/calldata.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,85 @@
use super::fuzz_param;
use crate::strategies::{fuzz_param, EvmFuzzState};
use alloy_dyn_abi::JsonAbiExt;
use alloy_json_abi::Function;
use alloy_primitives::Bytes;
use alloy_primitives::{Address, Bytes};
use foundry_config::FuzzDictionaryConfig;
use hashbrown::HashSet;
use proptest::prelude::{BoxedStrategy, Strategy};
use std::{fmt, sync::Arc};

/// Clonable wrapper around [CalldataFuzzDictionary].
#[derive(Debug, Clone)]
pub struct CalldataFuzzDictionary {
pub inner: Arc<CalldataFuzzDictionaryConfig>,
}

impl CalldataFuzzDictionary {
pub fn new(config: &FuzzDictionaryConfig, state: EvmFuzzState) -> Self {
Self { inner: Arc::new(CalldataFuzzDictionaryConfig::new(config, state)) }
}
}

#[derive(Clone)]
pub struct CalldataFuzzDictionaryConfig {
/// Addresses that can be used for fuzzing calldata.
pub addresses: Vec<Address>,
}

impl fmt::Debug for CalldataFuzzDictionaryConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CalldataFuzzDictionaryConfig").field("addresses", &self.addresses).finish()
}
}

/// Represents custom configuration for invariant fuzzed calldata strategies.
///
/// At the moment only the dictionary of addresses to be used for a fuzzed `function(address)` can
/// be configured, but support for other types can be added.
impl CalldataFuzzDictionaryConfig {
/// Creates config with the set of addresses that can be used for fuzzing invariant calldata (if
/// `max_calldata_fuzz_dictionary_addresses` configured).
/// The set of addresses contains a number of `max_calldata_fuzz_dictionary_addresses` random
/// addresses plus all addresses that already had their PUSH bytes collected (retrieved from
/// `EvmFuzzState`, if `include_push_bytes` config enabled).
pub fn new(config: &FuzzDictionaryConfig, state: EvmFuzzState) -> Self {
let mut addresses: HashSet<Address> = HashSet::new();
Copy link
Member

Choose a reason for hiding this comment

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

please add some docs

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

added in b77dcb6

let dict_size = config.max_calldata_fuzz_dictionary_addresses;

if dict_size > 0 {
loop {
if addresses.len() == dict_size {
break
}
addresses.insert(Address::random());
}

// Add all addresses that already had their PUSH bytes collected.
let mut state = state.write();
addresses.extend(state.addresses());
}

Self { addresses: Vec::from_iter(addresses) }
}
}

/// Given a function, it returns a strategy which generates valid calldata
/// for that function's input types.
pub fn fuzz_calldata(func: Function) -> BoxedStrategy<Bytes> {
fuzz_calldata_with_config(func, None)
}

/// Given a function, it returns a strategy which generates valid calldata
/// for that function's input types, following custom configuration rules.
pub fn fuzz_calldata_with_config(
func: Function,
config: Option<CalldataFuzzDictionary>,
) -> BoxedStrategy<Bytes> {
// 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.selector_type().parse().unwrap()))
.map(|input| fuzz_param(&input.selector_type().parse().unwrap(), config.clone()))
.collect::<Vec<_>>();

strats
Expand Down
34 changes: 27 additions & 7 deletions crates/evm/fuzz/src/strategies/invariants.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::fuzz_param_from_state;
use super::{fuzz_calldata_with_config, fuzz_param_from_state, CalldataFuzzDictionary};
use crate::{
invariant::{BasicTxDetails, FuzzRunIdentifiedContracts, SenderFilters},
strategies::{fuzz_calldata, fuzz_calldata_from_state, fuzz_param, EvmFuzzState},
strategies::{fuzz_calldata_from_state, fuzz_param, EvmFuzzState},
};
use alloy_json_abi::{Function, JsonAbi};
use alloy_primitives::{Address, Bytes};
Expand All @@ -14,6 +14,7 @@ pub fn override_call_strat(
fuzz_state: EvmFuzzState,
contracts: FuzzRunIdentifiedContracts,
target: Arc<RwLock<Address>>,
calldata_fuzz_config: CalldataFuzzDictionary,
) -> SBoxedStrategy<(Address, Bytes)> {
let contracts_ref = contracts.clone();

Expand All @@ -27,10 +28,16 @@ pub fn override_call_strat(
])
.prop_flat_map(move |target_address| {
let fuzz_state = fuzz_state.clone();
let calldata_fuzz_config = calldata_fuzz_config.clone();
let (_, abi, functions) = contracts.lock().get(&target_address).unwrap().clone();
let func = select_random_function(abi, functions);
func.prop_flat_map(move |func| {
fuzz_contract_with_calldata(fuzz_state.clone(), target_address, func)
fuzz_contract_with_calldata(
fuzz_state.clone(),
calldata_fuzz_config.clone(),
target_address,
func,
)
})
})
.sboxed()
Expand All @@ -51,10 +58,12 @@ pub fn invariant_strat(
senders: SenderFilters,
contracts: FuzzRunIdentifiedContracts,
dictionary_weight: u32,
calldata_fuzz_config: CalldataFuzzDictionary,
) -> impl Strategy<Value = Vec<BasicTxDetails>> {
// We only want to seed the first value, since we want to generate the rest as we mutate the
// state
generate_call(fuzz_state, senders, contracts, dictionary_weight).prop_map(|x| vec![x])
generate_call(fuzz_state, senders, contracts, dictionary_weight, calldata_fuzz_config)
.prop_map(|x| vec![x])
}

/// Strategy to generate a transaction where the `sender`, `target` and `calldata` are all generated
Expand All @@ -64,6 +73,7 @@ fn generate_call(
senders: SenderFilters,
contracts: FuzzRunIdentifiedContracts,
dictionary_weight: u32,
calldata_fuzz_config: CalldataFuzzDictionary,
) -> BoxedStrategy<BasicTxDetails> {
let random_contract = select_random_contract(contracts);
let senders = Rc::new(senders);
Expand All @@ -72,10 +82,19 @@ fn generate_call(
let func = select_random_function(abi, functions);
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.clone(), senders.clone(), dictionary_weight);
(sender, fuzz_contract_with_calldata(fuzz_state.clone(), contract, func))
(
sender,
fuzz_contract_with_calldata(
fuzz_state.clone(),
calldata_fuzz_config.clone(),
contract,
func,
),
)
})
})
.boxed()
Expand All @@ -93,7 +112,7 @@ fn select_random_sender(
let fuzz_strategy = proptest::strategy::Union::new_weighted(vec![
(
100 - dictionary_weight,
fuzz_param(&alloy_dyn_abi::DynSolType::Address)
fuzz_param(&alloy_dyn_abi::DynSolType::Address, None)
.prop_map(move |addr| addr.as_address().unwrap())
.boxed(),
),
Expand Down Expand Up @@ -165,6 +184,7 @@ fn select_random_function(
/// for that function's input types.
pub fn fuzz_contract_with_calldata(
fuzz_state: EvmFuzzState,
calldata_fuzz_config: CalldataFuzzDictionary,
contract: Address,
func: Function,
) -> impl Strategy<Value = (Address, Bytes)> {
Expand All @@ -173,7 +193,7 @@ pub fn fuzz_contract_with_calldata(
// `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning
#[allow(clippy::arc_with_non_send_sync)]
let strats = prop_oneof![
60 => fuzz_calldata(func.clone()),
60 => fuzz_calldata_with_config(func.clone(), Some(calldata_fuzz_config)),
40 => fuzz_calldata_from_state(func, fuzz_state),
];
strats.prop_map(move |calldata| {
Expand Down
4 changes: 3 additions & 1 deletion crates/evm/fuzz/src/strategies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ mod param;
pub use param::{fuzz_param, fuzz_param_from_state};

mod calldata;
pub use calldata::fuzz_calldata;
pub use calldata::{
fuzz_calldata, fuzz_calldata_with_config, CalldataFuzzDictionary, CalldataFuzzDictionaryConfig,
};

mod state;
pub use state::{
Expand Down
52 changes: 40 additions & 12 deletions crates/evm/fuzz/src/strategies/param.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::state::EvmFuzzState;
use crate::strategies::calldata::CalldataFuzzDictionary;
use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_primitives::{Address, FixedBytes, I256, U256};
use arbitrary::Unstructured;
Expand All @@ -10,12 +11,32 @@ const MAX_ARRAY_LEN: usize = 256;
/// Given a parameter type, returns a strategy for generating values for that type.
///
/// Works with ABI Encoder v2 tuples.
pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
pub fn fuzz_param(
param: &DynSolType,
config: Option<CalldataFuzzDictionary>,
) -> BoxedStrategy<DynSolValue> {
let param = param.to_owned();
match param {
DynSolType::Address => any::<[u8; 32]>()
.prop_map(|x| DynSolValue::Address(Address::from_word(x.into())))
.boxed(),
DynSolType::Address => {
if config.is_some() {
let fuzz_config = config.unwrap().inner;
let address_dict_len = fuzz_config.addresses.len();
if address_dict_len > 0 {
// Create strategy to return random address from configured dictionary.
return any::<prop::sample::Index>()
.prop_map(move |index| index.index(address_dict_len))
.prop_map(move |index| {
DynSolValue::Address(fuzz_config.addresses.get(index).cloned().unwrap())
})
.boxed()
}
}

// If no config for addresses dictionary then create unbounded addresses strategy.
any::<[u8; 32]>()
.prop_map(|x| DynSolValue::Address(Address::from_word(x.into())))
.boxed()
}
DynSolType::Int(n) => {
let strat = super::IntStrategy::new(n, vec![]);
let strat = strat.prop_map(move |x| DynSolValue::Int(x, n));
Expand Down Expand Up @@ -48,15 +69,22 @@ pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
)
})
.boxed(),
DynSolType::Tuple(params) => {
params.iter().map(fuzz_param).collect::<Vec<_>>().prop_map(DynSolValue::Tuple).boxed()
}
DynSolType::FixedArray(param, size) => proptest::collection::vec(fuzz_param(&param), size)
.prop_map(DynSolValue::FixedArray)
.boxed(),
DynSolType::Array(param) => proptest::collection::vec(fuzz_param(&param), 0..MAX_ARRAY_LEN)
.prop_map(DynSolValue::Array)
DynSolType::Tuple(params) => params
.iter()
.map(|p| fuzz_param(p, config.clone()))
.collect::<Vec<_>>()
.prop_map(DynSolValue::Tuple)
.boxed(),
DynSolType::FixedArray(param, size) => {
proptest::collection::vec(fuzz_param(&param, config), size)
.prop_map(DynSolValue::FixedArray)
.boxed()
}
DynSolType::Array(param) => {
proptest::collection::vec(fuzz_param(&param, config), 0..MAX_ARRAY_LEN)
.prop_map(DynSolValue::Array)
.boxed()
}
DynSolType::CustomStruct { .. } => panic!("unsupported type"),
}
}
Expand Down
Loading
Loading