Skip to content

Commit a243d36

Browse files
committed
Prefer fixed invariant sender pool
1 parent 74f820e commit a243d36

1 file changed

Lines changed: 82 additions & 17 deletions

File tree

crates/evm/fuzz/src/strategies/invariants.rs

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,27 @@ use crate::{
77
},
88
};
99
use alloy_json_abi::Function;
10-
use alloy_primitives::{Address, U256};
10+
use alloy_primitives::{Address, U256, address};
1111
use foundry_config::InvariantConfig;
12+
use foundry_evm_core::constants::CALLER;
1213
use parking_lot::RwLock;
1314
use proptest::prelude::*;
1415
use rand::seq::IteratorRandom;
1516
use std::{rc::Rc, sync::Arc};
1617

18+
/// Default invariant senders, modeled after Echidna's fixed sender pool.
19+
///
20+
/// The Foundry default deployer is included because owner/deployer-only paths are common in
21+
/// invariant targets.
22+
const DEFAULT_INVARIANT_SENDERS: [Address; 3] = [
23+
address!("0x0000000000000000000000000000000000010000"),
24+
address!("0x0000000000000000000000000000000000020000"),
25+
CALLER,
26+
];
27+
28+
const RANDOM_SENDER_WEIGHT: u32 = 1;
29+
const DEFAULT_SENDER_WEIGHT: u32 = 99;
30+
1731
/// Given a target address, we generate random calldata.
1832
pub fn override_call_strat(
1933
fuzz_state: EvmFuzzState,
@@ -132,40 +146,61 @@ pub fn invariant_strat(
132146
}
133147

134148
/// Strategy to select a sender address:
135-
/// * If `senders` is empty, then it's either a random address or one sampled from the dictionary
136-
/// according to the configured dictionary weight.
149+
/// * If `senders` is empty, then it is usually sampled from Foundry's fixed default sender pool.
150+
/// A random or dictionary address is used rarely to preserve broad exploration.
137151
/// * If `senders` is not empty, a random address is chosen from the list of senders.
138152
fn select_random_sender<S: FuzzStateReader>(
139153
fuzz_state: &S,
140154
senders: Rc<SenderFilters>,
141155
dictionary_weight: u32,
142156
) -> impl Strategy<Value = Address> + use<S> {
143157
if senders.targeted.is_empty() {
158+
let default_senders = default_invariant_senders(&senders);
144159
let dictionary_weight = dictionary_weight.min(100);
145-
proptest::prop_oneof![
160+
let random_sender = proptest::prop_oneof![
146161
100 - dictionary_weight => fuzz_param(&alloy_dyn_abi::DynSolType::Address),
147162
dictionary_weight => fuzz_param_from_state(&alloy_dyn_abi::DynSolType::Address, fuzz_state),
148163
]
149-
.prop_map(move |addr| {
150-
let mut addr = addr.as_address().unwrap();
151-
// Make sure the selected address is not in the list of excluded senders.
152-
// We don't use proptest's filter to avoid reaching the `PROPTEST_MAX_LOCAL_REJECTS`
153-
// max rejects and exiting test before all runs completes.
154-
// See <https://github.com/foundry-rs/foundry/issues/11369>.
155-
loop {
156-
if !senders.excluded.contains(&addr) {
157-
break;
164+
.prop_map({
165+
let senders = senders.clone();
166+
move |addr| {
167+
let mut addr = addr.as_address().unwrap();
168+
// Make sure the selected address is not in the list of excluded senders.
169+
// We don't use proptest's filter to avoid reaching the `PROPTEST_MAX_LOCAL_REJECTS`
170+
// max rejects and exiting test before all runs completes.
171+
// See <https://github.com/foundry-rs/foundry/issues/11369>.
172+
loop {
173+
if !senders.excluded.contains(&addr) {
174+
break;
175+
}
176+
addr = Address::random();
158177
}
159-
addr = Address::random();
178+
addr
160179
}
161-
addr
162-
})
163-
.boxed()
180+
});
181+
182+
if default_senders.is_empty() {
183+
random_sender.boxed()
184+
} else {
185+
proptest::prop_oneof![
186+
DEFAULT_SENDER_WEIGHT => any::<prop::sample::Index>()
187+
.prop_map(move |index| *index.get(&default_senders)),
188+
RANDOM_SENDER_WEIGHT => random_sender,
189+
]
190+
.boxed()
191+
}
164192
} else {
165193
any::<prop::sample::Index>().prop_map(move |index| *index.get(&senders.targeted)).boxed()
166194
}
167195
}
168196

197+
fn default_invariant_senders(senders: &SenderFilters) -> Vec<Address> {
198+
DEFAULT_INVARIANT_SENDERS
199+
.into_iter()
200+
.filter(|sender| !senders.excluded.contains(sender))
201+
.collect()
202+
}
203+
169204
/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata
170205
/// for that function's input types.
171206
pub fn fuzz_contract_with_calldata<S: FuzzStateReader>(
@@ -196,3 +231,33 @@ pub fn fuzz_contract_with_calldata<S: FuzzStateReader>(
196231
CallDetails { target, calldata, value }
197232
})
198233
}
234+
235+
#[cfg(test)]
236+
mod tests {
237+
use super::*;
238+
239+
#[test]
240+
fn default_sender_pool_includes_foundry_deployer() {
241+
let senders = SenderFilters::default();
242+
243+
assert_eq!(
244+
default_invariant_senders(&senders),
245+
vec![
246+
address!("0x0000000000000000000000000000000000010000"),
247+
address!("0x0000000000000000000000000000000000020000"),
248+
CALLER,
249+
]
250+
);
251+
}
252+
253+
#[test]
254+
fn default_sender_pool_respects_exclusions() {
255+
let excluded = address!("0x0000000000000000000000000000000000010000");
256+
let senders = SenderFilters::new(vec![], vec![excluded, CALLER]);
257+
258+
assert_eq!(
259+
default_invariant_senders(&senders),
260+
vec![address!("0x0000000000000000000000000000000000020000")]
261+
);
262+
}
263+
}

0 commit comments

Comments
 (0)