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