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