1use alloy_json_abi::{Event, Function, JsonAbi};
2use alloy_primitives::{Address, B256, Selector, map::HashMap};
3use foundry_compilers::artifacts::StorageLayout;
4use itertools::Either;
5use serde::{Deserialize, Serialize};
6use std::{
7 cell::{Cell, Ref, RefCell},
8 collections::BTreeMap,
9 fmt,
10 rc::Rc,
11 sync::Arc,
12};
13
14mod call_override;
15pub use call_override::RandomCallGenerator;
16
17mod filters;
18use crate::BasicTxDetails;
19pub use filters::{ArtifactFilters, SenderFilters};
20use foundry_common::{ContractsByAddress, ContractsByArtifact};
21use foundry_evm_core::utils::StateChangeset;
22
23type DynamicTargetCacheKey = (Address, B256);
24type DynamicTargetArtifactMatchCache =
25 Rc<RefCell<HashMap<DynamicTargetCacheKey, Option<CachedTargetContract>>>>;
26type FuzzedFunction = (Address, Function);
27type FunctionLookup = HashMap<Selector, Function>;
28
29pub fn is_optimization_invariant(func: &Function) -> bool {
32 func.outputs.len() == 1 && func.outputs[0].ty == "int256"
33}
34
35#[derive(Clone, Debug)]
40pub struct FuzzRunIdentifiedContracts {
41 targets: Rc<RefCell<TargetedContracts>>,
43 fuzzed_functions: Rc<RefCell<Vec<FuzzedFunction>>>,
45 fuzzed_functions_generation: Rc<Cell<u64>>,
47 pub is_updatable: bool,
49 artifact_matches: DynamicTargetArtifactMatchCache,
50}
51
52impl FuzzRunIdentifiedContracts {
53 pub fn new(targets: TargetedContracts, is_updatable: bool) -> Self {
55 let fuzzed_functions = Self::flatten_fuzzed_functions(&targets);
56 Self {
57 targets: Rc::new(RefCell::new(targets)),
58 fuzzed_functions: Rc::new(RefCell::new(fuzzed_functions)),
59 fuzzed_functions_generation: Rc::new(Cell::new(0)),
60 is_updatable,
61 artifact_matches: Rc::new(RefCell::new(HashMap::default())),
62 }
63 }
64
65 pub fn targets(&self) -> Ref<'_, TargetedContracts> {
67 self.targets.borrow()
68 }
69
70 pub fn fuzzed_functions(&self) -> Ref<'_, [FuzzedFunction]> {
72 Ref::map(self.fuzzed_functions.borrow(), Vec::as_slice)
73 }
74
75 pub fn fuzzed_functions_generation(&self) -> u64 {
77 self.fuzzed_functions_generation.get()
78 }
79
80 fn refresh_fuzzed_functions(&self) {
81 let fuzzed_functions = {
82 let targets = self.targets.borrow();
83 Self::flatten_fuzzed_functions(&targets)
84 };
85 *self.fuzzed_functions.borrow_mut() = fuzzed_functions;
86 self.fuzzed_functions_generation.set(self.fuzzed_functions_generation.get() + 1);
87 }
88
89 fn flatten_fuzzed_functions(targets: &TargetedContracts) -> Vec<FuzzedFunction> {
90 targets.fuzzed_functions().map(|(address, function)| (*address, function.clone())).collect()
91 }
92
93 pub fn collect_created_contracts(
96 &self,
97 state_changeset: &StateChangeset,
98 project_contracts: &ContractsByArtifact,
99 setup_contracts: &ContractsByAddress,
100 artifact_filters: &ArtifactFilters,
101 created_contracts: &mut Vec<Address>,
102 ) -> eyre::Result<()> {
103 if !self.is_updatable {
104 return Ok(());
105 }
106
107 let mut targets_changed = false;
108 for (address, account) in state_changeset {
109 if setup_contracts.contains_key(address) {
110 continue;
111 }
112 if !account.is_touched() {
113 continue;
114 }
115 let Some(code) = &account.info.code else {
116 continue;
117 };
118 if code.is_empty() {
119 continue;
120 }
121 let code_hash = code.hash_slow();
122 let code = code.original_byte_slice();
123 let Some(contract) = self.target_contract_for_code(
124 *address,
125 code_hash,
126 code,
127 project_contracts,
128 artifact_filters,
129 )?
130 else {
131 continue;
132 };
133 created_contracts.push(*address);
134 self.targets.borrow_mut().insert(*address, contract.into_targeted_contract());
135 targets_changed = true;
136 }
137 if targets_changed {
138 self.refresh_fuzzed_functions();
139 }
140 Ok(())
141 }
142
143 fn target_contract_for_code(
144 &self,
145 address: Address,
146 code_hash: B256,
147 code: &[u8],
148 project_contracts: &ContractsByArtifact,
149 artifact_filters: &ArtifactFilters,
150 ) -> eyre::Result<Option<CachedTargetContract>> {
151 let cache_key = (address, code_hash);
152 if let Some(cached_match) = self.artifact_matches.borrow().get(&cache_key) {
153 return Ok(cached_match.clone());
154 }
155
156 let cached_match = if let Some((artifact, contract_data)) =
157 project_contracts.find_by_deployed_code(code)
158 {
159 artifact_filters.get_targeted_functions(artifact, &contract_data.abi)?.map(
160 |targeted_functions| CachedTargetContract {
161 identifier: artifact.name.clone(),
162 abi: contract_data.abi.clone(),
163 targeted_functions,
164 storage_layout: contract_data.storage_layout.as_ref().map(Arc::clone),
165 event_lookup: Arc::new(TargetedContractEvents::new(&contract_data.abi)),
166 },
167 )
168 } else {
169 None
170 };
171 self.artifact_matches.borrow_mut().insert(cache_key, cached_match.clone());
172 Ok(cached_match)
173 }
174
175 pub fn clear_created_contracts(&self, created_contracts: Vec<Address>) {
177 let mut targets_changed = false;
178 if !created_contracts.is_empty() {
179 let mut targets = self.targets.borrow_mut();
180 for addr in &created_contracts {
181 targets_changed |= targets.remove(addr).is_some();
182 }
183 }
184 if targets_changed {
185 self.refresh_fuzzed_functions();
186 }
187 }
188}
189
190#[derive(Clone, Debug)]
191struct CachedTargetContract {
192 identifier: String,
193 abi: JsonAbi,
194 targeted_functions: Vec<Function>,
195 storage_layout: Option<Arc<StorageLayout>>,
196 event_lookup: Arc<TargetedContractEvents>,
197}
198
199impl CachedTargetContract {
200 fn into_targeted_contract(self) -> TargetedContract {
201 TargetedContract::from_parts(
202 self.identifier,
203 self.abi,
204 self.targeted_functions,
205 Vec::new(),
206 self.storage_layout,
207 self.event_lookup,
208 )
209 }
210}
211
212#[derive(Clone, Debug, Default)]
214pub struct TargetedContracts {
215 pub inner: BTreeMap<Address, TargetedContract>,
217}
218
219impl TargetedContracts {
220 pub fn new() -> Self {
222 Self::default()
223 }
224
225 pub fn fuzzed_artifacts(
229 &self,
230 tx: &BasicTxDetails,
231 ) -> (Option<&TargetedContract>, Option<&Function>) {
232 match self.inner.get(&tx.call_details.target) {
233 Some(c) => {
234 let function = tx
235 .call_details
236 .calldata
237 .get(..4)
238 .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
239 .map(Selector::from)
240 .and_then(|selector| c.function_by_selector(selector));
241 (Some(c), function)
242 }
243 None => (None, None),
244 }
245 }
246
247 pub fn fuzzed_functions(&self) -> impl Iterator<Item = (&Address, &Function)> {
250 self.inner
251 .iter()
252 .filter(|(_, c)| !c.abi.functions.is_empty())
253 .flat_map(|(contract, c)| c.abi_fuzzed_functions().map(move |f| (contract, f)))
254 }
255
256 pub fn can_replay(&self, tx: &BasicTxDetails) -> bool {
258 match self.inner.get(&tx.call_details.target) {
259 Some(c) => tx
260 .call_details
261 .calldata
262 .get(..4)
263 .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
264 .map(Selector::from)
265 .is_some_and(|selector| c.fuzzed_function_by_selector(selector).is_some()),
266 None => false,
267 }
268 }
269
270 pub fn fuzzed_metric_key(&self, tx: &BasicTxDetails) -> Option<String> {
273 tx.call_details
274 .calldata
275 .get(..4)
276 .and_then(|selector| <[u8; 4]>::try_from(selector).ok())
277 .map(Selector::from)
278 .and_then(|selector| {
279 self.fuzzed_metric_key_for_selector(tx.call_details.target, selector)
280 })
281 }
282
283 pub fn fuzzed_metric_key_for_selector(
286 &self,
287 target: Address,
288 selector: Selector,
289 ) -> Option<String> {
290 self.inner.get(&target).and_then(|contract| {
291 contract
292 .function_by_selector(selector)
293 .map(|function| format!("{}.{}", contract.identifier.as_str(), function.name))
294 })
295 }
296}
297
298impl std::ops::Deref for TargetedContracts {
299 type Target = BTreeMap<Address, TargetedContract>;
300
301 fn deref(&self) -> &Self::Target {
302 &self.inner
303 }
304}
305
306impl std::ops::DerefMut for TargetedContracts {
307 fn deref_mut(&mut self) -> &mut Self::Target {
308 &mut self.inner
309 }
310}
311
312#[derive(Clone, Debug)]
314pub struct TargetedContract {
315 pub identifier: String,
317 pub abi: JsonAbi,
319 pub targeted_functions: Vec<Function>,
321 pub excluded_functions: Vec<Function>,
323 pub storage_layout: Option<Arc<StorageLayout>>,
325 pub event_lookup: Arc<TargetedContractEvents>,
327 functions_by_selector: FunctionLookup,
328 fuzzed_functions_by_selector: FunctionLookup,
329}
330
331impl TargetedContract {
332 pub fn new(identifier: String, abi: JsonAbi) -> Self {
334 let event_lookup = Arc::new(TargetedContractEvents::new(&abi));
335 Self::from_parts(identifier, abi, Vec::new(), Vec::new(), None, event_lookup)
336 }
337
338 fn from_parts(
339 identifier: String,
340 abi: JsonAbi,
341 targeted_functions: Vec<Function>,
342 excluded_functions: Vec<Function>,
343 storage_layout: Option<Arc<StorageLayout>>,
344 event_lookup: Arc<TargetedContractEvents>,
345 ) -> Self {
346 let mut contract = Self {
347 identifier,
348 abi,
349 targeted_functions,
350 excluded_functions,
351 storage_layout,
352 event_lookup,
353 functions_by_selector: FunctionLookup::default(),
354 fuzzed_functions_by_selector: FunctionLookup::default(),
355 };
356 contract.rebuild_function_lookups();
357 contract
358 }
359
360 pub fn with_project_contracts(mut self, project_contracts: &ContractsByArtifact) -> Self {
363 if let Some((src, name)) = self.identifier.split_once(':')
364 && let Some((_, contract_data)) = project_contracts.iter().find(|(artifact, _)| {
365 artifact.name == name && artifact.source.as_path().ends_with(src)
366 })
367 {
368 self.storage_layout = contract_data.storage_layout.as_ref().map(Arc::clone);
369 }
370 self
371 }
372
373 pub fn abi_fuzzed_functions(&self) -> impl Iterator<Item = &Function> {
377 if self.targeted_functions.is_empty() {
378 Either::Right(self.abi.functions().filter(|&func| {
379 !matches!(
380 func.state_mutability,
381 alloy_json_abi::StateMutability::Pure | alloy_json_abi::StateMutability::View
382 ) && !self.excluded_functions.contains(func)
383 }))
384 } else {
385 Either::Left(
386 self.targeted_functions
387 .iter()
388 .filter(|func| !self.excluded_functions.contains(func)),
389 )
390 }
391 }
392
393 pub fn rebuild_function_lookups(&mut self) {
394 let functions_by_selector =
395 self.abi.functions().fold(FunctionLookup::default(), |mut functions, function| {
396 functions.entry(function.selector()).or_insert_with(|| function.clone());
397 functions
398 });
399 let fuzzed_functions_by_selector = self.abi_fuzzed_functions().fold(
400 FunctionLookup::default(),
401 |mut functions, function| {
402 functions.entry(function.selector()).or_insert_with(|| function.clone());
403 functions
404 },
405 );
406 self.functions_by_selector = functions_by_selector;
407 self.fuzzed_functions_by_selector = fuzzed_functions_by_selector;
408 }
409
410 pub fn function_by_selector(&self, selector: Selector) -> Option<&Function> {
412 self.functions_by_selector.get(&selector)
413 }
414
415 pub fn fuzzed_function_by_selector(&self, selector: Selector) -> Option<&Function> {
417 self.fuzzed_functions_by_selector.get(&selector)
418 }
419
420 pub fn get_function(&self, selector: Selector) -> eyre::Result<&Function> {
422 self.function_by_selector(selector)
423 .ok_or_else(|| eyre::eyre!("{} does not have the selector {selector}", self.identifier))
424 }
425
426 pub fn add_selectors(
428 &mut self,
429 selectors: impl IntoIterator<Item = Selector>,
430 should_exclude: bool,
431 ) -> eyre::Result<()> {
432 for selector in selectors {
433 if should_exclude {
434 self.excluded_functions.push(self.get_function(selector)?.clone());
435 } else {
436 self.targeted_functions.push(self.get_function(selector)?.clone());
437 }
438 }
439 self.rebuild_function_lookups();
440 Ok(())
441 }
442}
443
444#[derive(Clone, Debug, Default)]
446pub struct TargetedContractEvents {
447 by_topic: HashMap<(B256, usize), Vec<TargetedContractEvent>>,
448 anonymous: Vec<TargetedContractEvent>,
449}
450
451impl TargetedContractEvents {
452 fn new(abi: &JsonAbi) -> Self {
453 let mut events = Self::default();
454 for (order, event) in abi.events().enumerate() {
455 let event = TargetedContractEvent { order, event: event.clone() };
456 if event.event.anonymous {
457 events.anonymous.push(event);
458 } else {
459 let indexed_count = event.event.inputs.iter().filter(|input| input.indexed).count();
460 events
461 .by_topic
462 .entry((event.event.selector(), indexed_count))
463 .or_default()
464 .push(event);
465 }
466 }
467 events
468 }
469
470 pub fn by_topic(
471 &self,
472 selector: &B256,
473 indexed_count: usize,
474 ) -> Option<&[TargetedContractEvent]> {
475 self.by_topic.get(&(*selector, indexed_count)).map(Vec::as_slice)
476 }
477
478 pub fn anonymous(&self) -> &[TargetedContractEvent] {
479 &self.anonymous
480 }
481}
482
483#[derive(Clone, Debug)]
485pub struct TargetedContractEvent {
486 event: Event,
487 order: usize,
488}
489
490impl TargetedContractEvent {
491 pub const fn order(&self) -> usize {
492 self.order
493 }
494
495 pub const fn event(&self) -> &Event {
496 &self.event
497 }
498}
499
500#[derive(Clone, Debug)]
502pub struct InvariantContract<'a> {
503 pub address: Address,
505 pub name: &'a str,
507 pub invariant_fns: Vec<(&'a Function, bool)>,
511 pub anchor_idx: usize,
515 pub call_after_invariant: bool,
517 pub abi: &'a JsonAbi,
519}
520
521impl<'a> InvariantContract<'a> {
522 pub const fn new(
526 address: Address,
527 name: &'a str,
528 invariant_fns: Vec<(&'a Function, bool)>,
529 anchor_idx: usize,
530 call_after_invariant: bool,
531 abi: &'a JsonAbi,
532 ) -> Self {
533 Self { address, name, invariant_fns, anchor_idx, call_after_invariant, abi }
534 }
535
536 pub fn anchor(&self) -> &'a Function {
538 self.invariant_fns[self.anchor_idx].0
539 }
540
541 pub fn is_optimization(&self) -> bool {
543 is_optimization_invariant(self.anchor())
544 }
545}
546
547#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
553pub struct InvariantSettings {
554 pub target_contracts: BTreeMap<Address, String>,
556 pub target_selectors: BTreeMap<Address, Vec<Selector>>,
558 pub target_senders: Vec<Address>,
560 pub excluded_senders: Vec<Address>,
562 pub fail_on_revert: bool,
564}
565
566impl InvariantSettings {
567 pub fn new(
569 targeted_contracts: &TargetedContracts,
570 sender_filters: &SenderFilters,
571 fail_on_revert: bool,
572 ) -> Self {
573 let mut target_contracts = BTreeMap::new();
574 let mut target_selectors = BTreeMap::new();
575 for (addr, contract) in &targeted_contracts.inner {
576 target_contracts.insert(*addr, contract.identifier.clone());
577 target_selectors
578 .insert(*addr, contract.abi_fuzzed_functions().map(|f| f.selector()).collect());
579 }
580
581 let mut target_senders = sender_filters.targeted.clone();
582 target_senders.sort_unstable();
583
584 let mut excluded_senders = sender_filters.excluded.clone();
585 excluded_senders.sort_unstable();
586
587 Self {
588 target_contracts,
589 target_selectors,
590 target_senders,
591 excluded_senders,
592 fail_on_revert,
593 }
594 }
595
596 pub fn diff(&self, other: &Self) -> Option<String> {
599 let mut changes = Vec::new();
600
601 if self.target_contracts != other.target_contracts {
602 let added: Vec<_> = other
603 .target_contracts
604 .iter()
605 .filter(|(addr, _)| !self.target_contracts.contains_key(*addr))
606 .map(|(_, name)| name.as_str())
607 .collect();
608 let removed: Vec<_> = self
609 .target_contracts
610 .iter()
611 .filter(|(addr, _)| !other.target_contracts.contains_key(*addr))
612 .map(|(_, name)| name.as_str())
613 .collect();
614
615 if !added.is_empty() {
616 changes.push(format!("added target contracts: {}", added.join(", ")));
617 }
618 if !removed.is_empty() {
619 changes.push(format!("removed target contracts: {}", removed.join(", ")));
620 }
621 }
622
623 if self.target_selectors != other.target_selectors {
624 changes.push("target selectors changed".to_string());
625 }
626
627 if self.target_senders != other.target_senders {
628 changes.push("target senders changed".to_string());
629 }
630
631 if self.excluded_senders != other.excluded_senders {
632 changes.push("excluded senders changed".to_string());
633 }
634
635 if self.fail_on_revert != other.fail_on_revert {
636 changes.push(format!(
637 "fail_on_revert changed from {} to {}",
638 self.fail_on_revert, other.fail_on_revert
639 ));
640 }
641
642 if changes.is_empty() { None } else { Some(changes.join(", ")) }
643 }
644}
645
646impl fmt::Display for InvariantSettings {
647 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648 write!(
649 f,
650 "targets: {}, selectors: {}, senders: {}, excluded: {}, fail_on_revert: {}",
651 self.target_contracts.len(),
652 self.target_selectors.values().map(|v| v.len()).sum::<usize>(),
653 self.target_senders.len(),
654 self.excluded_senders.len(),
655 self.fail_on_revert,
656 )
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use crate::CallDetails;
664 use alloy_primitives::{Bytes, U256};
665 use foundry_compilers::{
666 ArtifactId,
667 artifacts::{
668 BytecodeObject, CompactBytecode, CompactContractBytecode, CompactDeployedBytecode,
669 },
670 };
671 use revm::{bytecode::Bytecode, state::Account};
672
673 fn abi_with_functions(functions: &[&str]) -> JsonAbi {
674 let mut abi = JsonAbi::new();
675 for function in functions {
676 let function = Function::parse(function).unwrap();
677 abi.functions.entry(function.name.clone()).or_default().push(function);
678 }
679 abi
680 }
681
682 fn targeted_contracts_with_functions(target: Address, functions: &[&str]) -> TargetedContracts {
683 let mut targets = TargetedContracts::new();
684 targets.inner.insert(
685 target,
686 TargetedContract::new("Target".to_string(), abi_with_functions(functions)),
687 );
688 targets
689 }
690
691 fn targeted_contracts_with_function(target: Address, function: Function) -> TargetedContracts {
692 let mut abi = JsonAbi::new();
693 abi.functions.entry(function.name.clone()).or_default().push(function);
694 let mut targets = TargetedContracts::new();
695 targets.inner.insert(target, TargetedContract::new("Target".to_string(), abi));
696 targets
697 }
698
699 fn tx(target: Address, calldata: impl Into<Bytes>) -> BasicTxDetails {
700 BasicTxDetails {
701 warp: None,
702 roll: None,
703 sender: Address::ZERO,
704 call_details: CallDetails { target, calldata: calldata.into(), value: None },
705 }
706 }
707
708 fn artifact_id(name: &str) -> ArtifactId {
709 ArtifactId {
710 path: format!("{name}.json").into(),
711 name: name.to_string(),
712 source: format!("{name}.sol").into(),
713 version: "0.8.30".parse().unwrap(),
714 build_id: "test".to_string(),
715 profile: "test".to_string(),
716 }
717 }
718
719 fn project_contracts_with_runtime_code(name: &str, code: Bytes) -> ContractsByArtifact {
720 project_contracts_with_runtime_code_and_abi(name, code, JsonAbi::new())
721 }
722
723 fn project_contracts_with_runtime_code_and_abi(
724 name: &str,
725 code: Bytes,
726 abi: JsonAbi,
727 ) -> ContractsByArtifact {
728 let deployed_bytecode = CompactDeployedBytecode {
729 bytecode: Some(CompactBytecode {
730 object: BytecodeObject::Bytecode(code),
731 source_map: None,
732 link_references: Default::default(),
733 }),
734 immutable_references: Default::default(),
735 };
736 let artifact = CompactContractBytecode {
737 abi: Some(abi),
738 bytecode: None,
739 deployed_bytecode: Some(deployed_bytecode),
740 };
741 ContractsByArtifact::new([(artifact_id(name), artifact)])
742 }
743
744 fn touched_account_with_code(code: Bytes) -> Account {
745 let mut account = Account::default();
746 account.info.balance = U256::ZERO;
747 account.info.code = Some(Bytecode::new_raw(code));
748 account.mark_touch();
749 account
750 }
751
752 #[test]
753 fn targeted_contracts_short_calldata_is_not_replayable_or_decodable() {
754 let target = Address::from([0x42; 20]);
755 let targets = targeted_contracts_with_function(target, Function::parse("foo()").unwrap());
756 let tx = tx(target, vec![0xde, 0xad, 0xbe]);
757
758 assert!(!targets.can_replay(&tx));
759 assert!(targets.fuzzed_artifacts(&tx).1.is_none());
760 assert!(targets.fuzzed_metric_key(&tx).is_none());
761 }
762
763 #[test]
764 fn abi_fuzzed_functions_filters_excluded_targeted_functions() {
765 let allowed = Function::parse("allowed()").unwrap();
766 let excluded = Function::parse("excluded()").unwrap();
767 let mut contract = TargetedContract::new("Target".to_string(), JsonAbi::new());
768 contract.targeted_functions = vec![allowed.clone(), excluded.clone()];
769 contract.excluded_functions = vec![excluded];
770
771 let selectors = contract.abi_fuzzed_functions().map(Function::selector).collect::<Vec<_>>();
772
773 assert_eq!(selectors, vec![allowed.selector()]);
774 }
775
776 #[test]
777 fn targeted_contracts_refresh_selector_lookup_after_filters() {
778 let target = Address::from([0x42; 20]);
779 let foo = Function::parse("foo()").unwrap();
780 let bar = Function::parse("bar()").unwrap();
781
782 let mut excluded = targeted_contracts_with_functions(target, &["foo()", "bar()"]);
783 excluded.inner.get_mut(&target).unwrap().add_selectors([foo.selector()], true).unwrap();
784 assert!(!excluded.can_replay(&tx(target, foo.selector().to_vec())));
785 assert!(excluded.can_replay(&tx(target, bar.selector().to_vec())));
786 assert_eq!(
787 excluded.fuzzed_artifacts(&tx(target, foo.selector().to_vec())).1.unwrap().name,
788 "foo"
789 );
790
791 let mut targeted = targeted_contracts_with_functions(target, &["foo()", "bar()"]);
792 targeted.inner.get_mut(&target).unwrap().add_selectors([foo.selector()], false).unwrap();
793 assert!(targeted.can_replay(&tx(target, foo.selector().to_vec())));
794 assert!(!targeted.can_replay(&tx(target, bar.selector().to_vec())));
795 assert_eq!(
796 targeted.fuzzed_metric_key_for_selector(target, bar.selector()).unwrap(),
797 "Target.bar"
798 );
799 }
800
801 #[test]
802 fn fuzz_run_identified_contracts_cache_fuzzed_functions_in_target_order() {
803 let first = Address::from([0x01; 20]);
804 let second = Address::from([0x02; 20]);
805 let mut targets = targeted_contracts_with_functions(second, &["bar()", "baz(uint256)"]);
806 targets.inner.insert(
807 first,
808 TargetedContract::new(
809 "First".to_string(),
810 abi_with_functions(&["foo()", "qux(address)"]),
811 ),
812 );
813 let expected = targets
814 .fuzzed_functions()
815 .map(|(address, function)| (*address, function.selector()))
816 .collect::<Vec<_>>();
817
818 let identified = FuzzRunIdentifiedContracts::new(targets, true);
819 let actual = identified
820 .fuzzed_functions()
821 .iter()
822 .map(|(address, function)| (*address, function.selector()))
823 .collect::<Vec<_>>();
824
825 assert_eq!(actual, expected);
826 }
827
828 #[test]
829 fn collect_created_contracts_caches_deployed_code_matches() {
830 let existing = Address::from([0x42; 20]);
831 let created = Address::from([0x43; 20]);
832 let setup = Address::from([0x44; 20]);
833 let untouched = Address::from([0x45; 20]);
834 let runtime_code = Bytes::from_static(&[0x60, 0x00, 0x56]);
835 let project_contracts =
836 project_contracts_with_runtime_code("DynamicTarget", runtime_code.clone());
837 let mut targets = TargetedContracts::new();
838 for address in [existing, setup, untouched] {
839 targets.inner.insert(
840 address,
841 TargetedContract::new("AlreadyTargeted".to_string(), JsonAbi::new()),
842 );
843 }
844 let identified = FuzzRunIdentifiedContracts::new(targets, true);
845
846 let mut state_changeset = StateChangeset::default();
847 state_changeset.insert(existing, touched_account_with_code(runtime_code.clone()));
848 state_changeset.insert(setup, touched_account_with_code(runtime_code.clone()));
849 state_changeset.insert(untouched, Account::default());
850 state_changeset.insert(created, touched_account_with_code(runtime_code));
851 let mut created_contracts = Vec::new();
852 let setup_contracts =
853 ContractsByAddress::from([(setup, ("Setup".to_string(), JsonAbi::new()))]);
854
855 identified
856 .collect_created_contracts(
857 &state_changeset,
858 &project_contracts,
859 &setup_contracts,
860 &ArtifactFilters::default(),
861 &mut created_contracts,
862 )
863 .unwrap();
864
865 created_contracts.sort_unstable();
866 assert_eq!(created_contracts, vec![existing, created]);
867 let targets = identified.targets();
868 assert_eq!(targets[&existing].identifier, "DynamicTarget");
869 assert_eq!(targets[&created].identifier, "DynamicTarget");
870 assert_eq!(targets[&setup].identifier, "AlreadyTargeted");
871 assert_eq!(targets[&untouched].identifier, "AlreadyTargeted");
872 drop(targets);
873
874 identified
875 .collect_created_contracts(
876 &state_changeset,
877 &Default::default(),
878 &setup_contracts,
879 &ArtifactFilters::default(),
880 &mut created_contracts,
881 )
882 .unwrap();
883
884 created_contracts.sort_unstable();
885 assert_eq!(created_contracts, vec![existing, existing, created, created]);
886 }
887
888 #[test]
889 fn collect_and_clear_created_contracts_refresh_fuzzed_function_cache() {
890 let existing = Address::from([0x42; 20]);
891 let created = Address::from([0x43; 20]);
892 let runtime_code = Bytes::from_static(&[0x60, 0x00, 0x56]);
893 let project_contracts = project_contracts_with_runtime_code_and_abi(
894 "DynamicTarget",
895 runtime_code.clone(),
896 abi_with_functions(&["dynamic(uint256)"]),
897 );
898 let identified = FuzzRunIdentifiedContracts::new(
899 targeted_contracts_with_functions(existing, &["existing()"]),
900 true,
901 );
902
903 let initial = identified
904 .fuzzed_functions()
905 .iter()
906 .map(|(address, function)| (*address, function.selector()))
907 .collect::<Vec<_>>();
908 assert_eq!(initial, vec![(existing, Function::parse("existing()").unwrap().selector())]);
909 assert_eq!(identified.fuzzed_functions_generation(), 0);
910
911 let mut state_changeset = StateChangeset::default();
912 state_changeset.insert(created, touched_account_with_code(runtime_code));
913 let mut created_contracts = Vec::new();
914
915 identified
916 .collect_created_contracts(
917 &state_changeset,
918 &project_contracts,
919 &ContractsByAddress::default(),
920 &ArtifactFilters::default(),
921 &mut created_contracts,
922 )
923 .unwrap();
924
925 let with_created = identified
926 .fuzzed_functions()
927 .iter()
928 .map(|(address, function)| (*address, function.selector()))
929 .collect::<Vec<_>>();
930 assert_eq!(identified.fuzzed_functions_generation(), 1);
931 assert_eq!(
932 with_created,
933 vec![
934 (existing, Function::parse("existing()").unwrap().selector()),
935 (created, Function::parse("dynamic(uint256)").unwrap().selector()),
936 ]
937 );
938
939 identified.clear_created_contracts(created_contracts);
940 let cleared = identified
941 .fuzzed_functions()
942 .iter()
943 .map(|(address, function)| (*address, function.selector()))
944 .collect::<Vec<_>>();
945 assert_eq!(cleared, initial);
946 assert_eq!(identified.fuzzed_functions_generation(), 2);
947 }
948}