1use super::string::parse;
4use crate::{
5 Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, inspector::exec_create,
6};
7use alloy_dyn_abi::DynSolType;
8use alloy_json_abi::ContractObject;
9use alloy_network::{Network, ReceiptResponse};
10use alloy_primitives::{Bytes, FixedBytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::{contracts::ContractData, fs};
15use foundry_config::fs_permissions::FsAccessKind;
16use foundry_evm_core::{FoundryTransaction, env::FoundryContextExt, evm::FoundryEvmNetwork};
17use revm::{
18 context::{Cfg, ContextTr, CreateScheme, JournalTr},
19 interpreter::CreateInputs,
20};
21use revm_inspectors::tracing::types::CallKind;
22use semver::Version;
23use std::{
24 io::{BufRead, BufReader},
25 path::{Path, PathBuf},
26 process::Command,
27 sync::mpsc,
28 thread,
29 time::{SystemTime, UNIX_EPOCH},
30};
31use walkdir::WalkDir;
32
33#[derive(Debug, Default, PartialEq, Eq)]
35struct ParsedArtifactPath<'a> {
36 file: Option<PathBuf>,
37 contract_name: Option<&'a str>,
38 version: Option<Version>,
39 profile: Option<&'a str>,
40}
41
42fn parse_artifact_path(path: &str) -> std::result::Result<ParsedArtifactPath<'_>, String> {
55 let unprefixed = path.strip_prefix(r"\\?\").unwrap_or(path);
58 let drive_prefix_len = match unprefixed.as_bytes() {
59 [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic() => {
60 path.len() - unprefixed.len() + 2
61 }
62 _ => 0,
63 };
64 let mut parts = path[drive_prefix_len..].split(':');
65
66 let mut file = None;
67 let mut contract_name = None;
68 let mut version = None;
69 let mut profile = None;
70
71 let path_or_name = parts.next().unwrap();
72 let path_or_name = &path[..drive_prefix_len + path_or_name.len()];
73 if path_or_name.contains('.') {
74 file = Some(PathBuf::from(path_or_name));
75 if let Some(name_or_version_or_profile) = parts.next() {
76 if name_or_version_or_profile.contains('.')
77 || Version::parse(name_or_version_or_profile).is_ok()
78 {
79 version = Some(name_or_version_or_profile);
80 } else {
81 contract_name = Some(name_or_version_or_profile);
82 if let Some(version_or_profile) = parts.next() {
83 if version_or_profile.contains('.')
84 || Version::parse(version_or_profile).is_ok()
85 {
86 version = Some(version_or_profile);
87 } else {
88 profile = Some(version_or_profile);
89 }
90 }
91 }
92 }
93 } else {
94 contract_name = Some(path_or_name);
95 if let Some(version_or_profile) = parts.next() {
96 if version_or_profile.contains('.') || Version::parse(version_or_profile).is_ok() {
97 version = Some(version_or_profile);
98 } else {
99 profile = Some(version_or_profile);
100 }
101 }
102 }
103
104 let version = if let Some(version) = version {
105 Some(Version::parse(version).map_err(|e| format!("failed parsing version: {e}"))?)
106 } else {
107 None
108 };
109
110 Ok(ParsedArtifactPath { file, contract_name, version, profile })
111}
112
113impl Cheatcode for existsCall {
114 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
115 let Self { path } = self;
116 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
117 Ok(path.exists().abi_encode())
118 }
119}
120
121impl Cheatcode for fsMetadataCall {
122 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
123 let Self { path } = self;
124 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
125
126 let metadata = path.metadata()?;
127
128 let [modified, accessed, created] =
130 [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
131 time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
132 });
133
134 Ok(FsMetadata {
135 isDir: metadata.is_dir(),
136 isSymlink: metadata.is_symlink(),
137 length: U256::from(metadata.len()),
138 readOnly: metadata.permissions().readonly(),
139 modified: U256::from(modified),
140 accessed: U256::from(accessed),
141 created: U256::from(created),
142 }
143 .abi_encode())
144 }
145}
146
147impl Cheatcode for isDirCall {
148 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
149 let Self { path } = self;
150 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
151 Ok(path.is_dir().abi_encode())
152 }
153}
154
155impl Cheatcode for isFileCall {
156 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
157 let Self { path } = self;
158 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
159 Ok(path.is_file().abi_encode())
160 }
161}
162
163impl Cheatcode for projectRootCall {
164 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
165 let Self {} = self;
166 Ok(state.config.root.display().to_string().abi_encode())
167 }
168}
169
170impl Cheatcode for currentFilePathCall {
171 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
172 let Self {} = self;
173 let artifact = state
174 .config
175 .running_artifact
176 .as_ref()
177 .ok_or_else(|| fmt_err!("no running contract found"))?;
178 let relative = artifact.source.strip_prefix(&state.config.root).unwrap_or(&artifact.source);
179 Ok(relative.display().to_string().abi_encode())
180 }
181}
182
183impl Cheatcode for unixTimeCall {
184 fn apply<FEN: FoundryEvmNetwork>(&self, _state: &mut Cheatcodes<FEN>) -> Result {
185 let Self {} = self;
186 let difference = SystemTime::now()
187 .duration_since(UNIX_EPOCH)
188 .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
189 Ok(difference.as_millis().abi_encode())
190 }
191}
192
193impl Cheatcode for closeFileCall {
194 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
195 let Self { path } = self;
196 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
197
198 state.test_context.opened_read_files.remove(&path);
199
200 Ok(Default::default())
201 }
202}
203
204impl Cheatcode for copyFileCall {
205 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
206 let Self { from, to } = self;
207 let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
208 let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
209 state.config.ensure_not_foundry_toml(&to)?;
210
211 let n = fs::copy(from, to)?;
212 Ok(n.abi_encode())
213 }
214}
215
216impl Cheatcode for createDirCall {
217 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
218 let Self { path, recursive } = self;
219 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
220 if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
221 Ok(Default::default())
222 }
223}
224
225impl Cheatcode for readDir_0Call {
226 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
227 let Self { path } = self;
228 read_dir(state, path.as_ref(), 1, false)
229 }
230}
231
232impl Cheatcode for readDir_1Call {
233 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
234 let Self { path, maxDepth } = self;
235 read_dir(state, path.as_ref(), *maxDepth, false)
236 }
237}
238
239impl Cheatcode for readDir_2Call {
240 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
241 let Self { path, maxDepth, followLinks } = self;
242 read_dir(state, path.as_ref(), *maxDepth, *followLinks)
243 }
244}
245
246impl Cheatcode for readFileCall {
247 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
248 let Self { path } = self;
249 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
250 Ok(fs::locked_read_to_string(path)?.abi_encode())
251 }
252}
253
254impl Cheatcode for readFileBinaryCall {
255 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
256 let Self { path } = self;
257 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
258 Ok(fs::locked_read(path)?.abi_encode())
259 }
260}
261
262impl Cheatcode for readLineCall {
263 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
264 let Self { path } = self;
265 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
266
267 let reader = match state.test_context.opened_read_files.entry(path.clone()) {
269 Entry::Occupied(entry) => entry.into_mut(),
270 Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
271 };
272
273 let mut line: String = String::new();
274 reader.read_line(&mut line)?;
275
276 if line.ends_with('\n') {
278 line.pop();
279 if line.ends_with('\r') {
280 line.pop();
281 }
282 }
283
284 Ok(line.abi_encode())
285 }
286}
287
288impl Cheatcode for readLinkCall {
289 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
290 let Self { linkPath: path } = self;
291 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
292 let target = fs::read_link(path)?;
293 Ok(target.display().to_string().abi_encode())
294 }
295}
296
297impl Cheatcode for removeDirCall {
298 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
299 let Self { path, recursive } = self;
300 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
301 state.config.ensure_not_foundry_toml(&path)?;
302 if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
303 Ok(Default::default())
304 }
305}
306
307impl Cheatcode for removeFileCall {
308 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
309 let Self { path } = self;
310 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
311 state.config.ensure_not_foundry_toml(&path)?;
312
313 state.test_context.opened_read_files.remove(&path);
315
316 if state.fs_commit {
317 fs::remove_file(&path)?;
318 }
319
320 Ok(Default::default())
321 }
322}
323
324impl Cheatcode for writeFileCall {
325 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
326 let Self { path, data } = self;
327 write_file(state, path.as_ref(), data.as_bytes())
328 }
329}
330
331impl Cheatcode for writeFileBinaryCall {
332 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
333 let Self { path, data } = self;
334 write_file(state, path.as_ref(), data)
335 }
336}
337
338impl Cheatcode for writeLineCall {
339 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
340 let Self { path, data: line } = self;
341 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
342 state.config.ensure_not_foundry_toml(&path)?;
343
344 if state.fs_commit {
345 fs::locked_write_line(path, line)?;
346 }
347
348 Ok(Default::default())
349 }
350}
351
352impl Cheatcode for getArtifactPathByCodeCall {
353 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
354 let Self { code } = self;
355 let (artifact_id, _) = state
356 .config
357 .available_artifacts
358 .as_ref()
359 .and_then(|artifacts| artifacts.find_by_creation_code(code))
360 .ok_or_else(|| fmt_err!("no matching artifact found"))?;
361
362 Ok(artifact_id.path.to_string_lossy().abi_encode())
363 }
364}
365
366impl Cheatcode for getArtifactPathByDeployedCodeCall {
367 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
368 let Self { deployedCode } = self;
369 let (artifact_id, _) = state
370 .config
371 .available_artifacts
372 .as_ref()
373 .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
374 .ok_or_else(|| fmt_err!("no matching artifact found"))?;
375
376 Ok(artifact_id.path.to_string_lossy().abi_encode())
377 }
378}
379
380impl Cheatcode for getCodeCall {
381 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
382 let Self { artifactPath: path } = self;
383 Ok(get_artifact_code(state, path, false)?.abi_encode())
384 }
385}
386
387impl Cheatcode for getDeployedCodeCall {
388 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
389 let Self { artifactPath: path } = self;
390 Ok(get_artifact_code(state, path, true)?.abi_encode())
391 }
392}
393
394impl Cheatcode for getSelectorsCall {
395 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
396 let Self { artifactPath: path } = self;
397 let selectors: Vec<FixedBytes<4>> = match get_artifact_source(state, path)? {
398 ArtifactSource::InMemory(data) => data.abi.functions().map(|f| f.selector()).collect(),
399 ArtifactSource::Disk(path) => {
400 let data = read_artifact_file(state, &path)?;
401 let json: serde_json::Value = serde_json::from_str(&data)?;
404 let abi =
405 json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
406 let abi: alloy_json_abi::JsonAbi =
407 serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))?;
408 abi.functions().map(|f| f.selector()).collect()
409 }
410 };
411 Ok(selectors.abi_encode())
412 }
413}
414
415impl Cheatcode for deployCode_0Call {
416 fn apply_full<FEN: FoundryEvmNetwork>(
417 &self,
418 ccx: &mut CheatsCtxt<'_, '_, FEN>,
419 executor: &mut dyn CheatcodesExecutor<FEN>,
420 ) -> Result {
421 let Self { artifactPath: path } = self;
422 deploy_code(ccx, executor, path, None, None, None)
423 }
424}
425
426impl Cheatcode for deployCode_1Call {
427 fn apply_full<FEN: FoundryEvmNetwork>(
428 &self,
429 ccx: &mut CheatsCtxt<'_, '_, FEN>,
430 executor: &mut dyn CheatcodesExecutor<FEN>,
431 ) -> Result {
432 let Self { artifactPath: path, constructorArgs: args } = self;
433 deploy_code(ccx, executor, path, Some(args), None, None)
434 }
435}
436
437impl Cheatcode for deployCode_2Call {
438 fn apply_full<FEN: FoundryEvmNetwork>(
439 &self,
440 ccx: &mut CheatsCtxt<'_, '_, FEN>,
441 executor: &mut dyn CheatcodesExecutor<FEN>,
442 ) -> Result {
443 let Self { artifactPath: path, value } = self;
444 deploy_code(ccx, executor, path, None, Some(*value), None)
445 }
446}
447
448impl Cheatcode for deployCode_3Call {
449 fn apply_full<FEN: FoundryEvmNetwork>(
450 &self,
451 ccx: &mut CheatsCtxt<'_, '_, FEN>,
452 executor: &mut dyn CheatcodesExecutor<FEN>,
453 ) -> Result {
454 let Self { artifactPath: path, constructorArgs: args, value } = self;
455 deploy_code(ccx, executor, path, Some(args), Some(*value), None)
456 }
457}
458
459impl Cheatcode for deployCode_4Call {
460 fn apply_full<FEN: FoundryEvmNetwork>(
461 &self,
462 ccx: &mut CheatsCtxt<'_, '_, FEN>,
463 executor: &mut dyn CheatcodesExecutor<FEN>,
464 ) -> Result {
465 let Self { artifactPath: path, salt } = self;
466 deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
467 }
468}
469
470impl Cheatcode for deployCode_5Call {
471 fn apply_full<FEN: FoundryEvmNetwork>(
472 &self,
473 ccx: &mut CheatsCtxt<'_, '_, FEN>,
474 executor: &mut dyn CheatcodesExecutor<FEN>,
475 ) -> Result {
476 let Self { artifactPath: path, constructorArgs: args, salt } = self;
477 deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
478 }
479}
480
481impl Cheatcode for deployCode_6Call {
482 fn apply_full<FEN: FoundryEvmNetwork>(
483 &self,
484 ccx: &mut CheatsCtxt<'_, '_, FEN>,
485 executor: &mut dyn CheatcodesExecutor<FEN>,
486 ) -> Result {
487 let Self { artifactPath: path, value, salt } = self;
488 deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
489 }
490}
491
492impl Cheatcode for deployCode_7Call {
493 fn apply_full<FEN: FoundryEvmNetwork>(
494 &self,
495 ccx: &mut CheatsCtxt<'_, '_, FEN>,
496 executor: &mut dyn CheatcodesExecutor<FEN>,
497 ) -> Result {
498 let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
499 deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
500 }
501}
502
503fn deploy_code<FEN: FoundryEvmNetwork>(
506 ccx: &mut CheatsCtxt<'_, '_, FEN>,
507 executor: &mut dyn CheatcodesExecutor<FEN>,
508 path: &str,
509 constructor_args: Option<&Bytes>,
510 value: Option<U256>,
511 salt: Option<U256>,
512) -> Result {
513 let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
514
515 if let Some(broadcast) = &mut ccx.state.broadcast {
517 broadcast.deploy_from_code = true;
518 }
519
520 if let Some(args) = constructor_args {
521 bytecode.extend_from_slice(args);
522 }
523
524 let scheme =
525 if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
526
527 let depth = ccx.ecx.journal().depth();
530 let mut caller = ccx.caller;
531 if let Some(prank) = ccx.state.get_prank(depth).copied()
532 && depth >= prank.depth
533 && caller == prank.prank_caller
534 {
535 let prank_applied = if depth == prank.depth {
536 caller = prank.new_caller;
537 true
538 } else {
539 false
540 };
541 let prank_applied = if let Some(new_origin) = prank.new_origin {
542 ccx.ecx.tx_mut().set_caller(new_origin);
543 true
544 } else {
545 prank_applied
546 };
547
548 if prank_applied && let Some(applied_prank) = prank.first_time_applied() {
549 ccx.state.pranks.insert(depth, applied_prank);
550 }
551 }
552
553 let outcome = exec_create(
554 executor,
555 CreateInputs::new(
556 caller,
557 scheme,
558 value.unwrap_or(U256::ZERO),
559 bytecode.into(),
560 ccx.gas_limit,
561 0,
562 ),
563 ccx,
564 );
565
566 if let Some(prank) = ccx.state.get_prank(depth).copied()
568 && depth == prank.depth
569 {
570 ccx.ecx.tx_mut().set_caller(prank.prank_origin);
571 if prank.single_call {
572 std::mem::take(&mut ccx.state.pranks);
573 }
574 }
575
576 let outcome = outcome?;
577
578 if !outcome.result.result.is_ok() {
579 return Err(crate::Error::from(outcome.result.output));
580 }
581
582 let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
583
584 Ok(address.abi_encode())
585}
586
587enum ArtifactSource<'a> {
589 InMemory(&'a ContractData),
591 Disk(PathBuf),
593}
594
595fn get_artifact_source<'a, FEN: FoundryEvmNetwork>(
609 state: &'a Cheatcodes<FEN>,
610 path: &str,
611) -> Result<ArtifactSource<'a>> {
612 if path.ends_with(".json") {
613 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
614 return Ok(ArtifactSource::Disk(path));
615 }
616
617 let parsed =
618 parse_artifact_path(path).map_err(|e| fmt_err!("failed to parse artifact path: {e}"))?;
619 let ParsedArtifactPath { file, contract_name, version, profile } = parsed;
620 let file = file.map(|file| {
621 let cwd = state
622 .config
623 .running_artifact
624 .as_ref()
625 .and_then(|artifact| artifact.source.parent())
626 .unwrap_or(&state.config.paths.root);
627 let relative_cwd = cwd.strip_prefix(&state.config.paths.root).unwrap_or(cwd);
628 let has_matching_remapping = state.config.paths.remappings.iter().any(|remapping| {
629 remapping.context.as_ref().is_none_or(|context| relative_cwd.starts_with(context))
630 && file.strip_prefix(&remapping.name).is_ok()
631 });
632
633 if has_matching_remapping {
634 state.config.paths.resolve_library_import(cwd, &file).map_or(file, |resolved| {
635 resolved.strip_prefix(&state.config.paths.root).unwrap_or(&resolved).to_path_buf()
636 })
637 } else {
638 file
639 }
640 });
641
642 if let Some(artifacts) =
644 state.config.available_artifacts.as_ref().or(state.config.artifact_lookup.as_ref())
645 {
646 let ambiguous_file_profile =
647 file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
648 let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
649 artifacts
650 .iter()
651 .filter(|(id, _)| {
652 let id_name = id.name.split('.').next().unwrap();
654
655 if let Some(path) = &file
656 && !id.source.ends_with(path)
657 {
658 return false;
659 }
660 if let Some(ref version) = version
661 && (id.version.minor != version.minor
662 || id.version.major != version.major
663 || id.version.patch != version.patch)
664 {
665 return false;
666 }
667 if let Some(profile) = profile
668 && id.profile != profile
669 {
670 return false;
671 }
672 if let Some(name) = contract_name {
673 if treat_ambiguous_as_profile && ambiguous_file_profile {
674 return id.profile == name;
675 }
676
677 return id_name == name;
678 }
679
680 true
681 })
682 .collect()
683 };
684
685 let mut filtered = filter_artifacts(false);
686 if filtered.is_empty() && ambiguous_file_profile {
687 filtered = filter_artifacts(true);
688 }
689
690 let artifact = match &filtered[..] {
691 [] => None,
692 [artifact] => Some(Ok(*artifact)),
693 filtered => {
694 let mut filtered = filtered.to_vec();
695 Some(
697 state
698 .config
699 .running_artifact
700 .as_ref()
701 .and_then(|running| {
702 if version.is_none() {
704 filtered.retain(|(id, _)| id.version == running.version);
705
706 if filtered.len() == 1 {
708 return Some(filtered[0]);
709 }
710 }
711
712 if profile.is_none() {
714 filtered.retain(|(id, _)| id.profile == running.profile);
715
716 return (filtered.len() == 1).then(|| filtered[0]);
717 }
718
719 None
720 })
721 .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
722 )
723 }
724 };
725
726 if let Some(artifact) = artifact {
727 return Ok(ArtifactSource::InMemory(artifact?.1));
728 }
729 }
730
731 let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
733 (Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
734 (None, Some(contract_name)) => {
735 PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
736 }
737 (Some(file), None) => {
738 let name = file.replace(".sol", "");
739 PathBuf::from(format!("{file}/{name}.json"))
740 }
741 _ => bail!("invalid artifact path"),
742 };
743
744 let path = state.config.paths.artifacts.join(path_in_artifacts);
745 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
746 Ok(ArtifactSource::Disk(path))
747}
748
749fn read_artifact_file<FEN: FoundryEvmNetwork>(
752 state: &Cheatcodes<FEN>,
753 path: &Path,
754) -> Result<String> {
755 fs::read_to_string(path).map_err(|e| {
756 if state.config.available_artifacts.is_some() {
757 fmt_err!("no matching artifact found")
758 } else {
759 e.into()
760 }
761 })
762}
763
764fn get_artifact_code<FEN: FoundryEvmNetwork>(
772 state: &Cheatcodes<FEN>,
773 path: &str,
774 deployed: bool,
775) -> Result<Bytes> {
776 let maybe_bytecode = match get_artifact_source(state, path)? {
777 ArtifactSource::InMemory(data) => {
778 if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
779 }
780 ArtifactSource::Disk(path) => {
781 let data = read_artifact_file(state, &path)?;
782 let artifact = serde_json::from_str::<ContractObject>(&data)?;
783 if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
784 }
785 };
786 maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
787}
788
789impl Cheatcode for ffiCall {
790 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
791 let Self { commandInput: input } = self;
792 let stdout = ffi_stdout(state, input)?;
793 Ok(decode_ffi_stdout(&stdout).abi_encode())
794 }
795}
796
797impl Cheatcode for ffiUintCall {
798 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
799 let Self { commandInput: input } = self;
800 parse(&ffi_stdout(state, input)?, &DynSolType::Uint(256))
801 }
802}
803
804impl Cheatcode for ffiStringCall {
805 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
806 let Self { commandInput: input } = self;
807 Ok(ffi_stdout(state, input)?.abi_encode())
808 }
809}
810
811impl Cheatcode for ffiBytesCall {
812 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
813 let Self { commandInput: input } = self;
814 let stdout = ffi_stdout(state, input)?;
815 Ok(hex::decode(&stdout)
816 .map_err(|err| fmt_err!("failed parsing ffi stdout as bytes: {err}"))?
817 .abi_encode())
818 }
819}
820
821impl Cheatcode for tryFfiCall {
822 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
823 let Self { commandInput: input } = self;
824 ffi(state, input).map(|res| res.abi_encode())
825 }
826}
827
828impl Cheatcode for promptCall {
829 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
830 let Self { promptText: text } = self;
831 prompt(state, text, prompt_input).map(|res| res.abi_encode())
832 }
833}
834
835impl Cheatcode for promptSecretCall {
836 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
837 let Self { promptText: text } = self;
838 prompt(state, text, prompt_password).map(|res| res.abi_encode())
839 }
840}
841
842impl Cheatcode for promptSecretUintCall {
843 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
844 let Self { promptText: text } = self;
845 parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
846 }
847}
848
849impl Cheatcode for promptAddressCall {
850 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
851 let Self { promptText: text } = self;
852 parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
853 }
854}
855
856impl Cheatcode for promptUintCall {
857 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
858 let Self { promptText: text } = self;
859 parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
860 }
861}
862
863pub(super) fn write_file<FEN: FoundryEvmNetwork>(
864 state: &Cheatcodes<FEN>,
865 path: &Path,
866 contents: &[u8],
867) -> Result {
868 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
869 state.config.ensure_not_foundry_toml(&path)?;
871
872 if state.fs_commit {
873 fs::locked_write(path, contents)?;
874 }
875
876 Ok(Default::default())
877}
878
879fn read_dir<FEN: FoundryEvmNetwork>(
880 state: &Cheatcodes<FEN>,
881 path: &Path,
882 max_depth: u64,
883 follow_links: bool,
884) -> Result {
885 let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
886 let paths: Vec<DirEntry> = WalkDir::new(root)
887 .min_depth(1)
888 .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
889 .follow_links(follow_links)
890 .contents_first(false)
891 .same_file_system(true)
892 .sort_by_file_name()
893 .into_iter()
894 .map(|entry| match entry {
895 Ok(entry) => DirEntry {
896 errorMessage: String::new(),
897 path: entry.path().display().to_string(),
898 depth: entry.depth() as u64,
899 isDir: entry.file_type().is_dir(),
900 isSymlink: entry.path_is_symlink(),
901 },
902 Err(e) => DirEntry {
903 errorMessage: e.to_string(),
904 path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
905 depth: e.depth() as u64,
906 isDir: false,
907 isSymlink: false,
908 },
909 })
910 .collect();
911 Ok(paths.abi_encode())
912}
913
914fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
915 let output = ffi_command(state, input)?;
916
917 let stdout = String::from_utf8(output.stdout)?;
918 let stdout = stdout.trim();
919 Ok(FfiResult {
920 exitCode: output.status.code().unwrap_or(69),
921 stdout: decode_ffi_stdout(stdout),
922 stderr: output.stderr.into(),
923 })
924}
925
926fn ffi_stdout<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<String> {
927 let output = ffi_command(state, input)?;
928 let stdout = String::from_utf8(output.stdout)?;
929 if !output.status.success() {
930 return Err(fmt_err!(
931 "ffi command {:?} exited with code {}. stderr: {}",
932 input,
933 output.status.code().unwrap_or(69),
934 String::from_utf8_lossy(&output.stderr)
935 ));
936 }
937 if !output.stderr.is_empty() {
938 let stderr = String::from_utf8_lossy(&output.stderr);
939 warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
940 }
941 Ok(stdout.trim().to_string())
942}
943
944fn decode_ffi_stdout(stdout: &str) -> Bytes {
945 hex::decode(stdout).unwrap_or_else(|_| stdout.as_bytes().to_vec()).into()
946}
947
948fn ffi_command<FEN: FoundryEvmNetwork>(
949 state: &Cheatcodes<FEN>,
950 input: &[String],
951) -> Result<std::process::Output> {
952 ensure!(
953 state.config.ffi,
954 "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
955 );
956 ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
957 let mut cmd = Command::new(&input[0]);
958 cmd.args(&input[1..]);
959
960 debug!(target: "cheatcodes", ?cmd, "invoking ffi");
961
962 cmd.current_dir(&state.config.root)
963 .output()
964 .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))
965}
966
967fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
968 Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
969}
970
971fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
972 Password::new().with_prompt(prompt_text).interact()
973}
974
975fn prompt<FEN: FoundryEvmNetwork>(
976 state: &Cheatcodes<FEN>,
977 prompt_text: &str,
978 input: fn(&str) -> Result<String, dialoguer::Error>,
979) -> Result<String> {
980 let text_clone = prompt_text.to_string();
981 let timeout = state.config.prompt_timeout;
982 let (tx, rx) = mpsc::channel();
983
984 thread::spawn(move || {
985 let _ = tx.send(input(&text_clone));
986 });
987
988 match rx.recv_timeout(timeout) {
989 Ok(res) => res.map_err(|err| {
990 let _ = sh_println!();
991 err.to_string().into()
992 }),
993 Err(_) => {
994 let _ = sh_eprintln!();
995 Err("Prompt timed out".into())
996 }
997 }
998}
999
1000impl Cheatcode for getBroadcastCall {
1001 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1002 let Self { contractName, chainId, txType } = self;
1003
1004 let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1005 contractName,
1006 *chainId,
1007 &state.config.broadcast,
1008 vec![map_broadcast_tx_type(*txType)],
1009 )?;
1010
1011 Ok(latest_broadcast.abi_encode())
1012 }
1013}
1014
1015impl Cheatcode for getBroadcasts_0Call {
1016 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1017 let Self { contractName, chainId, txType } = self;
1018
1019 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1020 .with_tx_type(map_broadcast_tx_type(*txType));
1021
1022 let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1023
1024 let summaries = broadcasts
1025 .into_iter()
1026 .flat_map(|broadcast| {
1027 let results = reader.into_tx_receipts(broadcast);
1028 parse_broadcast_results(results)
1029 })
1030 .collect::<Vec<_>>();
1031
1032 Ok(summaries.abi_encode())
1033 }
1034}
1035
1036impl Cheatcode for getBroadcasts_1Call {
1037 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1038 let Self { contractName, chainId } = self;
1039
1040 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
1041
1042 let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1043
1044 let summaries = broadcasts
1045 .into_iter()
1046 .flat_map(|broadcast| {
1047 let results = reader.into_tx_receipts(broadcast);
1048 parse_broadcast_results(results)
1049 })
1050 .collect::<Vec<_>>();
1051
1052 Ok(summaries.abi_encode())
1053 }
1054}
1055
1056impl Cheatcode for getDeployment_0Call {
1057 fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
1058 let Self { contractName } = self;
1059 let chain_id = ccx.ecx.cfg().chain_id();
1060
1061 let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1062 contractName,
1063 chain_id,
1064 &ccx.state.config.broadcast,
1065 vec![CallKind::Create, CallKind::Create2],
1066 )?;
1067
1068 Ok(latest_broadcast.contractAddress.abi_encode())
1069 }
1070}
1071
1072impl Cheatcode for getDeployment_1Call {
1073 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1074 let Self { contractName, chainId } = self;
1075
1076 let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1077 contractName,
1078 *chainId,
1079 &state.config.broadcast,
1080 vec![CallKind::Create, CallKind::Create2],
1081 )?;
1082
1083 Ok(latest_broadcast.contractAddress.abi_encode())
1084 }
1085}
1086
1087impl Cheatcode for getDeploymentsCall {
1088 fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1089 let Self { contractName, chainId } = self;
1090
1091 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1092 .with_tx_type(CallKind::Create)
1093 .with_tx_type(CallKind::Create2);
1094
1095 let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1096
1097 let summaries = broadcasts
1098 .into_iter()
1099 .flat_map(|broadcast| {
1100 let results = reader.into_tx_receipts(broadcast);
1101 parse_broadcast_results(results)
1102 })
1103 .collect::<Vec<_>>();
1104
1105 let deployed_addresses =
1106 summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
1107
1108 Ok(deployed_addresses.abi_encode())
1109 }
1110}
1111
1112fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
1113 match tx_type {
1114 BroadcastTxType::Call => CallKind::Call,
1115 BroadcastTxType::Create => CallKind::Create,
1116 BroadcastTxType::Create2 => CallKind::Create2,
1117 _ => unreachable!("invalid tx type"),
1118 }
1119}
1120
1121fn parse_broadcast_results<N: Network>(
1122 results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
1123) -> Vec<BroadcastTxSummary> {
1124 results
1125 .into_iter()
1126 .map(|(tx, receipt)| BroadcastTxSummary {
1127 txHash: receipt.transaction_hash(),
1128 blockNumber: receipt.block_number().unwrap_or_default(),
1129 txType: match tx.call_kind {
1130 CallKind::Call => BroadcastTxType::Call,
1131 CallKind::Create => BroadcastTxType::Create,
1132 CallKind::Create2 => BroadcastTxType::Create2,
1133 _ => unreachable!("invalid tx type"),
1134 },
1135 contractAddress: tx.contract_address.unwrap_or_default(),
1136 success: receipt.status(),
1137 })
1138 .collect()
1139}
1140
1141fn latest_broadcast<N: Network>(
1142 contract_name: &String,
1143 chain_id: u64,
1144 broadcast_path: &Path,
1145 filters: Vec<CallKind>,
1146) -> Result<BroadcastTxSummary>
1147where
1148 N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1149{
1150 let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1151
1152 for filter in filters {
1153 reader = reader.with_tx_type(filter);
1154 }
1155
1156 let broadcast = reader.read_latest::<N>()?;
1157
1158 let results = reader.into_tx_receipts(broadcast);
1159
1160 let summaries = parse_broadcast_results(results);
1161
1162 summaries
1163 .first()
1164 .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1165 .cloned()
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::*;
1171 use crate::CheatsConfig;
1172 use alloy_primitives::{address, b256};
1173 use foundry_common::ContractsByArtifact;
1174 use foundry_compilers::{
1175 ArtifactId,
1176 artifacts::{
1177 BytecodeObject, CompactBytecode, CompactContractBytecode, remappings::Remapping,
1178 },
1179 };
1180 use foundry_evm_core::evm::TempoEvmNetwork;
1181 use std::{env, fs as stdfs, str::FromStr, sync::Arc};
1182 use tempfile::TempDir;
1183
1184 fn cheats() -> Cheatcodes {
1185 let config = CheatsConfig {
1186 ffi: true,
1187 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1188 ..Default::default()
1189 };
1190 Cheatcodes::new(Arc::new(config))
1191 }
1192
1193 #[test]
1194 fn test_ffi_hex() {
1195 let msg = b"gm";
1196 let cheats = cheats();
1197 let args = ["echo".to_string(), hex::encode(msg)];
1198 let output = ffi(&cheats, &args).unwrap();
1199 assert_eq!(output.stdout, Bytes::from(msg));
1200 }
1201
1202 #[test]
1203 fn test_ffi_string() {
1204 let msg = "gm";
1205 let cheats = cheats();
1206 let args = ["echo".to_string(), msg.to_string()];
1207 let output = ffi(&cheats, &args).unwrap();
1208 assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1209 }
1210
1211 #[test]
1212 fn test_ffi_fails_on_error_code() {
1213 let mut cheats = cheats();
1214
1215 #[cfg(unix)]
1217 let args = vec!["false".to_string()];
1218 #[cfg(windows)]
1219 let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1220
1221 let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1222
1223 assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1225
1226 let err_msg = result.unwrap_err().to_string();
1228 assert!(
1229 err_msg.contains("exited with code 1"),
1230 "Error message did not contain exit code: {err_msg}"
1231 );
1232 }
1233
1234 #[test]
1235 fn test_artifact_parsing() {
1236 let s = include_str!("../../evm/test-data/solc-obj.json");
1237 let artifact: ContractObject = serde_json::from_str(s).unwrap();
1238 assert!(artifact.bytecode.is_some());
1239
1240 let artifact: ContractObject = serde_json::from_str(s).unwrap();
1241 assert!(artifact.deployed_bytecode.is_some());
1242 }
1243
1244 #[test]
1245 fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1246 let artifact_json = r#"{
1247 "abi": [],
1248 "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1249 }"#;
1250
1251 let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1252 assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1253 let err = result.unwrap_err().to_string();
1254 assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1255 }
1256
1257 #[test]
1258 fn test_parse_artifact_path_file_only() {
1259 let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1260 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1261 assert_eq!(parsed.contract_name, None);
1262 assert_eq!(parsed.version, None);
1263 assert_eq!(parsed.profile, None);
1264 }
1265
1266 #[test]
1267 fn test_parse_artifact_path_windows_drive() {
1268 for file in [
1269 "C:/project/src/Contract.sol",
1270 r"C:\project\src\Contract.sol",
1271 r"\\?\C:\project\src\Contract.sol",
1272 r"\\server\share\Contract.sol",
1273 ] {
1274 for (suffix, contract_name, version, profile) in [
1275 ("", None, None, None),
1276 (":MyContract", Some("MyContract"), None, None),
1277 (":0.8.23", None, Some(Version::new(0, 8, 23)), None),
1278 (":MyContract:0.8.23", Some("MyContract"), Some(Version::new(0, 8, 23)), None),
1279 (":MyContract:optimized", Some("MyContract"), None, Some("optimized")),
1280 ] {
1281 let input = format!("{file}{suffix}");
1282 assert_eq!(
1283 parse_artifact_path(&input).unwrap(),
1284 ParsedArtifactPath {
1285 file: Some(PathBuf::from(file)),
1286 contract_name,
1287 version,
1288 profile,
1289 },
1290 "{input}"
1291 );
1292 }
1293 }
1294
1295 assert_eq!(parse_artifact_path("C:0.8.23").unwrap().version, Some(Version::new(0, 8, 23)));
1297 assert_eq!(parse_artifact_path("C:optimized").unwrap().profile, Some("optimized"));
1298 }
1299
1300 #[test]
1301 fn test_parse_artifact_path_file_and_contract() {
1302 let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1303 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1304 assert_eq!(parsed.contract_name, Some("MyContract"));
1305 assert_eq!(parsed.version, None);
1306 assert_eq!(parsed.profile, None);
1307 }
1308
1309 #[test]
1310 fn test_parse_artifact_path_file_contract_version() {
1311 let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1312 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1313 assert_eq!(parsed.contract_name, Some("MyContract"));
1314 assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1315 assert_eq!(parsed.profile, None);
1316 }
1317
1318 #[test]
1319 fn test_parse_artifact_path_file_contract_profile() {
1320 let parsed =
1321 super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1322 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1323 assert_eq!(parsed.contract_name, Some("MyContract"));
1324 assert_eq!(parsed.version, None);
1325 assert_eq!(parsed.profile, Some("optimized"));
1326 }
1327
1328 #[test]
1329 fn test_parse_artifact_path_file_and_version() {
1330 let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1331 assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1332 assert_eq!(parsed.contract_name, None);
1333 assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1334 assert_eq!(parsed.profile, None);
1335 }
1336
1337 #[test]
1338 fn test_parse_artifact_path_file_and_profile() {
1339 let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1342 assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1343 assert_eq!(parsed.contract_name, Some("paris"));
1344 assert_eq!(parsed.version, None);
1345 assert_eq!(parsed.profile, None);
1346 }
1347
1348 fn test_artifact(
1349 source: &str,
1350 name: &str,
1351 profile: &str,
1352 bytecode: Bytes,
1353 ) -> (ArtifactId, CompactContractBytecode) {
1354 (
1355 ArtifactId {
1356 path: PathBuf::from(format!("{source}/{name}.json")),
1357 name: name.to_owned(),
1358 source: PathBuf::from(source),
1359 version: Version::new(0, 8, 30),
1360 build_id: String::new(),
1361 profile: profile.to_owned(),
1362 },
1363 CompactContractBytecode {
1364 abi: Some(Default::default()),
1365 bytecode: Some(CompactBytecode {
1366 object: BytecodeObject::Bytecode(bytecode),
1367 source_map: None,
1368 link_references: Default::default(),
1369 }),
1370 deployed_bytecode: None,
1371 },
1372 )
1373 }
1374
1375 #[test]
1376 fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1377 let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1378 let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1379 let source = "src/GetCodeProfile.t.sol";
1380 let artifacts = ContractsByArtifact::new([
1381 test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1382 test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1383 ]);
1384 let config = CheatsConfig {
1385 available_artifacts: Some(artifacts),
1386 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1387 ..Default::default()
1388 };
1389 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1390
1391 let bytecode =
1392 super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1393
1394 assert_eq!(bytecode, paris_bytecode);
1395 }
1396
1397 #[test]
1398 fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1399 let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1400 let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1401 let source = "src/GetCodeProfile.t.sol";
1402 let artifacts = ContractsByArtifact::new([
1403 test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1404 test_artifact(source, "paris", "default", contract_bytecode.clone()),
1405 ]);
1406 let config = CheatsConfig {
1407 available_artifacts: Some(artifacts),
1408 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1409 ..Default::default()
1410 };
1411 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1412
1413 let bytecode =
1414 super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1415
1416 assert_eq!(bytecode, contract_bytecode);
1417 }
1418
1419 #[test]
1420 fn test_get_artifact_code_resolves_remapping() {
1421 let bytecode = Bytes::from_static(&[0x60, 0x01]);
1422 let artifacts = ContractsByArtifact::new([test_artifact(
1423 "src/Something.sol",
1424 "Something",
1425 "default",
1426 bytecode.clone(),
1427 )]);
1428 let root = PathBuf::from(&env!("CARGO_MANIFEST_DIR"));
1429 let paths = foundry_compilers::ProjectPathsConfig::builder()
1430 .remapping(Remapping::from_str("@example/=src/").unwrap())
1431 .build_with_root(&root);
1432 let config = CheatsConfig {
1433 available_artifacts: Some(artifacts),
1434 root,
1435 paths,
1436 ..Default::default()
1437 };
1438 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1439
1440 let resolved =
1441 super::get_artifact_code(&cheats, "@example/Something.sol:Something", false).unwrap();
1442
1443 assert_eq!(resolved, bytecode);
1444 }
1445
1446 #[test]
1447 fn test_get_artifact_code_preserves_project_path_on_library_collision() {
1448 let root_bytecode = Bytes::from_static(&[0x60, 0x01]);
1449 let library_bytecode = Bytes::from_static(&[0x60, 0x02]);
1450 let artifacts = ContractsByArtifact::new([
1451 test_artifact("src/Thing.sol", "RootThing", "default", root_bytecode.clone()),
1452 test_artifact("lib/src/Thing.sol", "LibThing", "default", library_bytecode),
1453 ]);
1454 let temp = TempDir::new().unwrap();
1455 let library = temp.path().join("lib");
1456 stdfs::create_dir_all(library.join("src")).unwrap();
1457 stdfs::write(library.join("src/Thing.sol"), "").unwrap();
1458 let paths = foundry_compilers::ProjectPathsConfig::builder()
1459 .remappings([])
1460 .lib(library)
1461 .build_with_root(temp.path());
1462 let config = CheatsConfig {
1463 available_artifacts: Some(artifacts),
1464 root: temp.path().to_path_buf(),
1465 paths,
1466 ..Default::default()
1467 };
1468 let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1469
1470 let resolved = super::get_artifact_code(&cheats, "src/Thing.sol:RootThing", false).unwrap();
1471
1472 assert_eq!(resolved, root_bytecode);
1473 }
1474
1475 #[test]
1476 fn test_parse_artifact_path_contract_only() {
1477 let parsed = super::parse_artifact_path("MyContract").unwrap();
1478 assert_eq!(parsed.file, None);
1479 assert_eq!(parsed.contract_name, Some("MyContract"));
1480 assert_eq!(parsed.version, None);
1481 assert_eq!(parsed.profile, None);
1482 }
1483
1484 #[test]
1485 fn test_parse_artifact_path_contract_and_version() {
1486 let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1487 assert_eq!(parsed.file, None);
1488 assert_eq!(parsed.contract_name, Some("MyContract"));
1489 assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1490 assert_eq!(parsed.profile, None);
1491 }
1492
1493 #[test]
1494 fn test_parse_artifact_path_contract_and_profile() {
1495 let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1496 assert_eq!(parsed.file, None);
1497 assert_eq!(parsed.contract_name, Some("MyContract"));
1498 assert_eq!(parsed.version, None);
1499 assert_eq!(parsed.profile, Some("optimized"));
1500 }
1501
1502 #[test]
1503 fn test_parse_artifact_path_profile_names() {
1504 for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1506 let path = format!("MyContract:{profile}");
1507 let parsed = super::parse_artifact_path(&path).unwrap();
1508 assert_eq!(parsed.contract_name, Some("MyContract"));
1509 assert_eq!(parsed.profile, Some(profile));
1510 assert_eq!(parsed.version, None);
1511 }
1512 }
1513
1514 #[test]
1515 fn test_parse_artifact_path_invalid_version() {
1516 let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1518 assert_eq!(parsed.contract_name, Some("MyContract"));
1519 assert_eq!(parsed.profile, Some("invalid"));
1520 assert_eq!(parsed.version, None);
1521 }
1522
1523 fn unique_temp_dir(prefix: &str) -> PathBuf {
1524 env::temp_dir().join(format!(
1525 "foundry-cheatcodes-{prefix}-{}",
1526 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1527 ))
1528 }
1529
1530 #[test]
1531 fn test_latest_broadcast_reads_tempo_sequences() {
1532 let root = unique_temp_dir("tempo-broadcast");
1533 let broadcast_path = root.join("broadcast");
1534 let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1535 stdfs::create_dir_all(&sequence_dir).unwrap();
1536
1537 let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1538 let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1539 let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1540 let contract_address = "0x20c0000000000000000000000000000000000000";
1541 let zero_bloom = format!("0x{}", "0".repeat(512));
1542
1543 let sequence = serde_json::json!({
1544 "transactions": [{
1545 "hash": tx_hash,
1546 "transactionType": "CREATE",
1547 "contractName": "Counter",
1548 "contractAddress": contract_address,
1549 "function": serde_json::Value::Null,
1550 "arguments": serde_json::Value::Null,
1551 "transaction": {
1552 "type": "0x76",
1553 "from": from,
1554 "to": serde_json::Value::Null,
1555 "data": "0x",
1556 "value": "0x0",
1557 "gas": "0x5208",
1558 "nonce": "0x0",
1559 "accessList": [],
1560 "calls": [],
1561 "nonceKey": "0x0",
1562 "feePayerSignature": serde_json::Value::Null,
1563 "validBefore": serde_json::Value::Null,
1564 "validAfter": serde_json::Value::Null,
1565 "keyAuthorization": serde_json::Value::Null,
1566 "aaAuthorizationList": []
1567 },
1568 "additionalContracts": [],
1569 "isFixedGasLimit": false
1570 }],
1571 "receipts": [{
1572 "type": "0x76",
1573 "status": "0x1",
1574 "cumulativeGasUsed": "0x5208",
1575 "logs": [],
1576 "logsBloom": zero_bloom,
1577 "transactionHash": tx_hash,
1578 "transactionIndex": "0x0",
1579 "blockHash": block_hash,
1580 "blockNumber": "0x7",
1581 "gasUsed": "0x5208",
1582 "effectiveGasPrice": "0x1",
1583 "from": from,
1584 "to": serde_json::Value::Null,
1585 "contractAddress": contract_address,
1586 "feePayer": from
1587 }],
1588 "libraries": [],
1589 "pending": [],
1590 "returns": {},
1591 "timestamp": 1,
1592 "chain": 31337,
1593 "commit": serde_json::Value::Null
1594 });
1595
1596 fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1597
1598 let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1599 &"Counter".to_owned(),
1600 31337,
1601 &broadcast_path,
1602 vec![CallKind::Create],
1603 )
1604 .unwrap();
1605
1606 assert_eq!(
1607 latest.txHash,
1608 b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1609 );
1610 assert_eq!(latest.blockNumber, 7);
1611 assert!(matches!(latest.txType, BroadcastTxType::Create));
1612 assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1613 assert!(latest.success);
1614
1615 stdfs::remove_dir_all(root).unwrap();
1616 }
1617}