@@ -7,13 +7,27 @@ use crate::{
77 } ,
88} ;
99use alloy_json_abi:: Function ;
10- use alloy_primitives:: { Address , U256 } ;
10+ use alloy_primitives:: { Address , U256 , address } ;
1111use foundry_config:: InvariantConfig ;
12+ use foundry_evm_core:: constants:: CALLER ;
1213use parking_lot:: RwLock ;
1314use proptest:: prelude:: * ;
1415use rand:: seq:: IteratorRandom ;
1516use 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.
1832pub 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.
138152fn 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.
171206pub 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