1use super::string::parse;
4use crate::{
5 Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, EthCheatCtx, Result, Vm::*,
6 inspector::exec_create,
7};
8use alloy_dyn_abi::DynSolType;
9use alloy_json_abi::ContractObject;
10use alloy_network::{Ethereum, Network, ReceiptResponse};
11use alloy_primitives::{Bytes, U256, hex, map::Entry};
12use alloy_sol_types::SolValue;
13use dialoguer::{Input, Password};
14use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
15use foundry_common::fs;
16use foundry_config::fs_permissions::FsAccessKind;
17use revm::{
18 context::{Cfg, ContextTr, CreateScheme, JournalTr},
19 interpreter::CreateInputs,
20};
21use revm_inspectors::tracing::types::CallKind;
22use semver::Version;
23use std::{
24 io::{BufRead, BufReader},
25 path::{Path, PathBuf},
26 process::Command,
27 sync::mpsc,
28 thread,
29 time::{SystemTime, UNIX_EPOCH},
30};
31use walkdir::WalkDir;
32
33impl Cheatcode for existsCall {
34 fn apply(&self, state: &mut Cheatcodes) -> Result {
35 let Self { path } = self;
36 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
37 Ok(path.exists().abi_encode())
38 }
39}
40
41impl Cheatcode for fsMetadataCall {
42 fn apply(&self, state: &mut Cheatcodes) -> Result {
43 let Self { path } = self;
44 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
45
46 let metadata = path.metadata()?;
47
48 let [modified, accessed, created] =
50 [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
51 time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
52 });
53
54 Ok(FsMetadata {
55 isDir: metadata.is_dir(),
56 isSymlink: metadata.is_symlink(),
57 length: U256::from(metadata.len()),
58 readOnly: metadata.permissions().readonly(),
59 modified: U256::from(modified),
60 accessed: U256::from(accessed),
61 created: U256::from(created),
62 }
63 .abi_encode())
64 }
65}
66
67impl Cheatcode for isDirCall {
68 fn apply(&self, state: &mut Cheatcodes) -> Result {
69 let Self { path } = self;
70 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
71 Ok(path.is_dir().abi_encode())
72 }
73}
74
75impl Cheatcode for isFileCall {
76 fn apply(&self, state: &mut Cheatcodes) -> Result {
77 let Self { path } = self;
78 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
79 Ok(path.is_file().abi_encode())
80 }
81}
82
83impl Cheatcode for projectRootCall {
84 fn apply(&self, state: &mut Cheatcodes) -> Result {
85 let Self {} = self;
86 Ok(state.config.root.display().to_string().abi_encode())
87 }
88}
89
90impl Cheatcode for currentFilePathCall {
91 fn apply(&self, state: &mut Cheatcodes) -> Result {
92 let Self {} = self;
93 let artifact = state
94 .config
95 .running_artifact
96 .as_ref()
97 .ok_or_else(|| fmt_err!("no running contract found"))?;
98 let relative = artifact.source.strip_prefix(&state.config.root).unwrap_or(&artifact.source);
99 Ok(relative.display().to_string().abi_encode())
100 }
101}
102
103impl Cheatcode for unixTimeCall {
104 fn apply(&self, _state: &mut Cheatcodes) -> Result {
105 let Self {} = self;
106 let difference = SystemTime::now()
107 .duration_since(UNIX_EPOCH)
108 .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
109 Ok(difference.as_millis().abi_encode())
110 }
111}
112
113impl Cheatcode for closeFileCall {
114 fn apply(&self, state: &mut Cheatcodes) -> Result {
115 let Self { path } = self;
116 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
117
118 state.test_context.opened_read_files.remove(&path);
119
120 Ok(Default::default())
121 }
122}
123
124impl Cheatcode for copyFileCall {
125 fn apply(&self, state: &mut Cheatcodes) -> Result {
126 let Self { from, to } = self;
127 let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
128 let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
129 state.config.ensure_not_foundry_toml(&to)?;
130
131 let n = fs::copy(from, to)?;
132 Ok(n.abi_encode())
133 }
134}
135
136impl Cheatcode for createDirCall {
137 fn apply(&self, state: &mut Cheatcodes) -> Result {
138 let Self { path, recursive } = self;
139 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
140 if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
141 Ok(Default::default())
142 }
143}
144
145impl Cheatcode for readDir_0Call {
146 fn apply(&self, state: &mut Cheatcodes) -> Result {
147 let Self { path } = self;
148 read_dir(state, path.as_ref(), 1, false)
149 }
150}
151
152impl Cheatcode for readDir_1Call {
153 fn apply(&self, state: &mut Cheatcodes) -> Result {
154 let Self { path, maxDepth } = self;
155 read_dir(state, path.as_ref(), *maxDepth, false)
156 }
157}
158
159impl Cheatcode for readDir_2Call {
160 fn apply(&self, state: &mut Cheatcodes) -> Result {
161 let Self { path, maxDepth, followLinks } = self;
162 read_dir(state, path.as_ref(), *maxDepth, *followLinks)
163 }
164}
165
166impl Cheatcode for readFileCall {
167 fn apply(&self, state: &mut Cheatcodes) -> Result {
168 let Self { path } = self;
169 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
170 Ok(fs::locked_read_to_string(path)?.abi_encode())
171 }
172}
173
174impl Cheatcode for readFileBinaryCall {
175 fn apply(&self, state: &mut Cheatcodes) -> Result {
176 let Self { path } = self;
177 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
178 Ok(fs::locked_read(path)?.abi_encode())
179 }
180}
181
182impl Cheatcode for readLineCall {
183 fn apply(&self, state: &mut Cheatcodes) -> Result {
184 let Self { path } = self;
185 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
186
187 let reader = match state.test_context.opened_read_files.entry(path.clone()) {
189 Entry::Occupied(entry) => entry.into_mut(),
190 Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
191 };
192
193 let mut line: String = String::new();
194 reader.read_line(&mut line)?;
195
196 if line.ends_with('\n') {
198 line.pop();
199 if line.ends_with('\r') {
200 line.pop();
201 }
202 }
203
204 Ok(line.abi_encode())
205 }
206}
207
208impl Cheatcode for readLinkCall {
209 fn apply(&self, state: &mut Cheatcodes) -> Result {
210 let Self { linkPath: path } = self;
211 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
212 let target = fs::read_link(path)?;
213 Ok(target.display().to_string().abi_encode())
214 }
215}
216
217impl Cheatcode for removeDirCall {
218 fn apply(&self, state: &mut Cheatcodes) -> Result {
219 let Self { path, recursive } = self;
220 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
221 if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
222 Ok(Default::default())
223 }
224}
225
226impl Cheatcode for removeFileCall {
227 fn apply(&self, state: &mut Cheatcodes) -> Result {
228 let Self { path } = self;
229 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
230 state.config.ensure_not_foundry_toml(&path)?;
231
232 state.test_context.opened_read_files.remove(&path);
234
235 if state.fs_commit {
236 fs::remove_file(&path)?;
237 }
238
239 Ok(Default::default())
240 }
241}
242
243impl Cheatcode for writeFileCall {
244 fn apply(&self, state: &mut Cheatcodes) -> Result {
245 let Self { path, data } = self;
246 write_file(state, path.as_ref(), data.as_bytes())
247 }
248}
249
250impl Cheatcode for writeFileBinaryCall {
251 fn apply(&self, state: &mut Cheatcodes) -> Result {
252 let Self { path, data } = self;
253 write_file(state, path.as_ref(), data)
254 }
255}
256
257impl Cheatcode for writeLineCall {
258 fn apply(&self, state: &mut Cheatcodes) -> Result {
259 let Self { path, data: line } = self;
260 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
261 state.config.ensure_not_foundry_toml(&path)?;
262
263 if state.fs_commit {
264 fs::locked_write_line(path, line)?;
265 }
266
267 Ok(Default::default())
268 }
269}
270
271impl Cheatcode for getArtifactPathByCodeCall {
272 fn apply(&self, state: &mut Cheatcodes) -> Result {
273 let Self { code } = self;
274 let (artifact_id, _) = state
275 .config
276 .available_artifacts
277 .as_ref()
278 .and_then(|artifacts| artifacts.find_by_creation_code(code))
279 .ok_or_else(|| fmt_err!("no matching artifact found"))?;
280
281 Ok(artifact_id.path.to_string_lossy().abi_encode())
282 }
283}
284
285impl Cheatcode for getArtifactPathByDeployedCodeCall {
286 fn apply(&self, state: &mut Cheatcodes) -> Result {
287 let Self { deployedCode } = self;
288 let (artifact_id, _) = state
289 .config
290 .available_artifacts
291 .as_ref()
292 .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
293 .ok_or_else(|| fmt_err!("no matching artifact found"))?;
294
295 Ok(artifact_id.path.to_string_lossy().abi_encode())
296 }
297}
298
299impl Cheatcode for getCodeCall {
300 fn apply(&self, state: &mut Cheatcodes) -> Result {
301 let Self { artifactPath: path } = self;
302 Ok(get_artifact_code(state, path, false)?.abi_encode())
303 }
304}
305
306impl Cheatcode for getDeployedCodeCall {
307 fn apply(&self, state: &mut Cheatcodes) -> Result {
308 let Self { artifactPath: path } = self;
309 Ok(get_artifact_code(state, path, true)?.abi_encode())
310 }
311}
312
313impl Cheatcode for deployCode_0Call {
314 fn apply_full<CTX: EthCheatCtx>(
315 &self,
316 ccx: &mut CheatsCtxt<'_, CTX>,
317 executor: &mut dyn CheatcodesExecutor<CTX>,
318 ) -> Result {
319 let Self { artifactPath: path } = self;
320 deploy_code(ccx, executor, path, None, None, None)
321 }
322}
323
324impl Cheatcode for deployCode_1Call {
325 fn apply_full<CTX: EthCheatCtx>(
326 &self,
327 ccx: &mut CheatsCtxt<'_, CTX>,
328 executor: &mut dyn CheatcodesExecutor<CTX>,
329 ) -> Result {
330 let Self { artifactPath: path, constructorArgs: args } = self;
331 deploy_code(ccx, executor, path, Some(args), None, None)
332 }
333}
334
335impl Cheatcode for deployCode_2Call {
336 fn apply_full<CTX: EthCheatCtx>(
337 &self,
338 ccx: &mut CheatsCtxt<'_, CTX>,
339 executor: &mut dyn CheatcodesExecutor<CTX>,
340 ) -> Result {
341 let Self { artifactPath: path, value } = self;
342 deploy_code(ccx, executor, path, None, Some(*value), None)
343 }
344}
345
346impl Cheatcode for deployCode_3Call {
347 fn apply_full<CTX: EthCheatCtx>(
348 &self,
349 ccx: &mut CheatsCtxt<'_, CTX>,
350 executor: &mut dyn CheatcodesExecutor<CTX>,
351 ) -> Result {
352 let Self { artifactPath: path, constructorArgs: args, value } = self;
353 deploy_code(ccx, executor, path, Some(args), Some(*value), None)
354 }
355}
356
357impl Cheatcode for deployCode_4Call {
358 fn apply_full<CTX: EthCheatCtx>(
359 &self,
360 ccx: &mut CheatsCtxt<'_, CTX>,
361 executor: &mut dyn CheatcodesExecutor<CTX>,
362 ) -> Result {
363 let Self { artifactPath: path, salt } = self;
364 deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
365 }
366}
367
368impl Cheatcode for deployCode_5Call {
369 fn apply_full<CTX: EthCheatCtx>(
370 &self,
371 ccx: &mut CheatsCtxt<'_, CTX>,
372 executor: &mut dyn CheatcodesExecutor<CTX>,
373 ) -> Result {
374 let Self { artifactPath: path, constructorArgs: args, salt } = self;
375 deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
376 }
377}
378
379impl Cheatcode for deployCode_6Call {
380 fn apply_full<CTX: EthCheatCtx>(
381 &self,
382 ccx: &mut CheatsCtxt<'_, CTX>,
383 executor: &mut dyn CheatcodesExecutor<CTX>,
384 ) -> Result {
385 let Self { artifactPath: path, value, salt } = self;
386 deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
387 }
388}
389
390impl Cheatcode for deployCode_7Call {
391 fn apply_full<CTX: EthCheatCtx>(
392 &self,
393 ccx: &mut CheatsCtxt<'_, CTX>,
394 executor: &mut dyn CheatcodesExecutor<CTX>,
395 ) -> Result {
396 let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
397 deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
398 }
399}
400
401fn deploy_code<CTX: EthCheatCtx>(
404 ccx: &mut CheatsCtxt<'_, CTX>,
405 executor: &mut dyn CheatcodesExecutor<CTX>,
406 path: &str,
407 constructor_args: Option<&Bytes>,
408 value: Option<U256>,
409 salt: Option<U256>,
410) -> Result {
411 let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
412
413 if let Some(broadcast) = &mut ccx.state.broadcast {
415 broadcast.deploy_from_code = true;
416 }
417
418 if let Some(args) = constructor_args {
419 bytecode.extend_from_slice(args);
420 }
421
422 let scheme =
423 if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
424
425 let caller =
427 ccx.state.get_prank(ccx.ecx.journal().depth()).map_or(ccx.caller, |prank| prank.new_caller);
428
429 let outcome = exec_create(
430 executor,
431 CreateInputs::new(
432 caller,
433 scheme,
434 value.unwrap_or(U256::ZERO),
435 bytecode.into(),
436 ccx.gas_limit,
437 ),
438 ccx,
439 )?;
440
441 if !outcome.result.result.is_ok() {
442 return Err(crate::Error::from(outcome.result.output));
443 }
444
445 let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
446
447 Ok(address.abi_encode())
448}
449
450fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<Bytes> {
465 let path = if path.ends_with(".json") {
466 PathBuf::from(path)
467 } else {
468 let mut parts = path.split(':');
469
470 let mut file = None;
471 let mut contract_name = None;
472 let mut version = None;
473
474 let path_or_name = parts.next().unwrap();
475 if path_or_name.contains('.') {
476 file = Some(PathBuf::from(path_or_name));
477 if let Some(name_or_version) = parts.next() {
478 if name_or_version.contains('.') {
479 version = Some(name_or_version);
480 } else {
481 contract_name = Some(name_or_version);
482 version = parts.next();
483 }
484 }
485 } else {
486 contract_name = Some(path_or_name);
487 version = parts.next();
488 }
489
490 let version = if let Some(version) = version {
491 Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
492 } else {
493 None
494 };
495
496 if let Some(artifacts) = &state.config.available_artifacts {
498 let filtered = artifacts
499 .iter()
500 .filter(|(id, _)| {
501 let id_name = id.name.split('.').next().unwrap();
503
504 if let Some(path) = &file
505 && !id.source.ends_with(path)
506 {
507 return false;
508 }
509 if let Some(name) = contract_name
510 && id_name != name
511 {
512 return false;
513 }
514 if let Some(ref version) = version
515 && (id.version.minor != version.minor
516 || id.version.major != version.major
517 || id.version.patch != version.patch)
518 {
519 return false;
520 }
521 true
522 })
523 .collect::<Vec<_>>();
524
525 let artifact = match &filtered[..] {
526 [] => None,
527 [artifact] => Some(Ok(*artifact)),
528 filtered => {
529 let mut filtered = filtered.to_vec();
530 Some(
532 state
533 .config
534 .running_artifact
535 .as_ref()
536 .and_then(|running| {
537 filtered.retain(|(id, _)| id.version == running.version);
539
540 if filtered.len() == 1 {
542 return Some(filtered[0]);
543 }
544
545 filtered.retain(|(id, _)| id.profile == running.profile);
547
548 if filtered.len() == 1 { Some(filtered[0]) } else { None }
549 })
550 .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
551 )
552 }
553 };
554
555 if let Some(artifact) = artifact {
556 let artifact = artifact?;
557 let maybe_bytecode = if deployed {
558 artifact.1.deployed_bytecode().cloned()
559 } else {
560 artifact.1.bytecode().cloned()
561 };
562
563 return maybe_bytecode.ok_or_else(|| {
564 fmt_err!("no bytecode for contract; is it abstract or unlinked?")
565 });
566 }
567 }
568
569 let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name)
571 {
572 (Some(file), Some(contract_name)) => {
573 PathBuf::from(format!("{file}/{contract_name}.json"))
574 }
575 (None, Some(contract_name)) => {
576 PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
577 }
578 (Some(file), None) => {
579 let name = file.replace(".sol", "");
580 PathBuf::from(format!("{file}/{name}.json"))
581 }
582 _ => bail!("invalid artifact path"),
583 };
584
585 state.config.paths.artifacts.join(path_in_artifacts)
586 };
587
588 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
589 let data = fs::read_to_string(path).map_err(|e| {
590 if state.config.available_artifacts.is_some() {
591 fmt_err!("no matching artifact found")
592 } else {
593 e.into()
594 }
595 })?;
596 let artifact = serde_json::from_str::<ContractObject>(&data)?;
597 let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
598 maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
599}
600
601impl Cheatcode for ffiCall {
602 fn apply(&self, state: &mut Cheatcodes) -> Result {
603 let Self { commandInput: input } = self;
604
605 let output = ffi(state, input)?;
606
607 if output.exitCode != 0 {
609 return Err(fmt_err!(
611 "ffi command {:?} exited with code {}. stderr: {}",
612 input,
613 output.exitCode,
614 String::from_utf8_lossy(&output.stderr)
615 ));
616 }
617
618 if !output.stderr.is_empty() {
620 let stderr = String::from_utf8_lossy(&output.stderr);
621 warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
622 }
623
624 Ok(output.stdout.abi_encode())
626 }
627}
628
629impl Cheatcode for tryFfiCall {
630 fn apply(&self, state: &mut Cheatcodes) -> Result {
631 let Self { commandInput: input } = self;
632 ffi(state, input).map(|res| res.abi_encode())
633 }
634}
635
636impl Cheatcode for promptCall {
637 fn apply(&self, state: &mut Cheatcodes) -> Result {
638 let Self { promptText: text } = self;
639 prompt(state, text, prompt_input).map(|res| res.abi_encode())
640 }
641}
642
643impl Cheatcode for promptSecretCall {
644 fn apply(&self, state: &mut Cheatcodes) -> Result {
645 let Self { promptText: text } = self;
646 prompt(state, text, prompt_password).map(|res| res.abi_encode())
647 }
648}
649
650impl Cheatcode for promptSecretUintCall {
651 fn apply(&self, state: &mut Cheatcodes) -> Result {
652 let Self { promptText: text } = self;
653 parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
654 }
655}
656
657impl Cheatcode for promptAddressCall {
658 fn apply(&self, state: &mut Cheatcodes) -> Result {
659 let Self { promptText: text } = self;
660 parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
661 }
662}
663
664impl Cheatcode for promptUintCall {
665 fn apply(&self, state: &mut Cheatcodes) -> Result {
666 let Self { promptText: text } = self;
667 parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
668 }
669}
670
671pub(super) fn write_file(state: &Cheatcodes, path: &Path, contents: &[u8]) -> Result {
672 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
673 state.config.ensure_not_foundry_toml(&path)?;
675
676 if state.fs_commit {
677 fs::locked_write(path, contents)?;
678 }
679
680 Ok(Default::default())
681}
682
683fn read_dir(state: &Cheatcodes, path: &Path, max_depth: u64, follow_links: bool) -> Result {
684 let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
685 let paths: Vec<DirEntry> = WalkDir::new(root)
686 .min_depth(1)
687 .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
688 .follow_links(follow_links)
689 .contents_first(false)
690 .same_file_system(true)
691 .sort_by_file_name()
692 .into_iter()
693 .map(|entry| match entry {
694 Ok(entry) => DirEntry {
695 errorMessage: String::new(),
696 path: entry.path().display().to_string(),
697 depth: entry.depth() as u64,
698 isDir: entry.file_type().is_dir(),
699 isSymlink: entry.path_is_symlink(),
700 },
701 Err(e) => DirEntry {
702 errorMessage: e.to_string(),
703 path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
704 depth: e.depth() as u64,
705 isDir: false,
706 isSymlink: false,
707 },
708 })
709 .collect();
710 Ok(paths.abi_encode())
711}
712
713fn ffi(state: &Cheatcodes, input: &[String]) -> Result<FfiResult> {
714 ensure!(
715 state.config.ffi,
716 "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
717 );
718 ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
719 let mut cmd = Command::new(&input[0]);
720 cmd.args(&input[1..]);
721
722 debug!(target: "cheatcodes", ?cmd, "invoking ffi");
723
724 let output = cmd
725 .current_dir(&state.config.root)
726 .output()
727 .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
728
729 let trimmed_stdout = String::from_utf8(output.stdout)?;
732 let trimmed_stdout = trimmed_stdout.trim();
733 let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
734 hex
735 } else {
736 trimmed_stdout.as_bytes().to_vec()
737 };
738 Ok(FfiResult {
739 exitCode: output.status.code().unwrap_or(69),
740 stdout: encoded_stdout.into(),
741 stderr: output.stderr.into(),
742 })
743}
744
745fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
746 Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
747}
748
749fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
750 Password::new().with_prompt(prompt_text).interact()
751}
752
753fn prompt(
754 state: &Cheatcodes,
755 prompt_text: &str,
756 input: fn(&str) -> Result<String, dialoguer::Error>,
757) -> Result<String> {
758 let text_clone = prompt_text.to_string();
759 let timeout = state.config.prompt_timeout;
760 let (tx, rx) = mpsc::channel();
761
762 thread::spawn(move || {
763 let _ = tx.send(input(&text_clone));
764 });
765
766 match rx.recv_timeout(timeout) {
767 Ok(res) => res.map_err(|err| {
768 let _ = sh_println!();
769 err.to_string().into()
770 }),
771 Err(_) => {
772 let _ = sh_eprintln!();
773 Err("Prompt timed out".into())
774 }
775 }
776}
777
778impl Cheatcode for getBroadcastCall {
779 fn apply(&self, state: &mut Cheatcodes) -> Result {
780 let Self { contractName, chainId, txType } = self;
781
782 let latest_broadcast = latest_broadcast(
783 contractName,
784 *chainId,
785 &state.config.broadcast,
786 vec![map_broadcast_tx_type(*txType)],
787 )?;
788
789 Ok(latest_broadcast.abi_encode())
790 }
791}
792
793impl Cheatcode for getBroadcasts_0Call {
794 fn apply(&self, state: &mut Cheatcodes) -> Result {
795 let Self { contractName, chainId, txType } = self;
796
797 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
798 .with_tx_type(map_broadcast_tx_type(*txType));
799
800 let broadcasts = reader.read::<Ethereum>()?;
801
802 let summaries = broadcasts
803 .into_iter()
804 .flat_map(|broadcast| {
805 let results = reader.into_tx_receipts(broadcast);
806 parse_broadcast_results(results)
807 })
808 .collect::<Vec<_>>();
809
810 Ok(summaries.abi_encode())
811 }
812}
813
814impl Cheatcode for getBroadcasts_1Call {
815 fn apply(&self, state: &mut Cheatcodes) -> Result {
816 let Self { contractName, chainId } = self;
817
818 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
819
820 let broadcasts = reader.read::<Ethereum>()?;
821
822 let summaries = broadcasts
823 .into_iter()
824 .flat_map(|broadcast| {
825 let results = reader.into_tx_receipts(broadcast);
826 parse_broadcast_results(results)
827 })
828 .collect::<Vec<_>>();
829
830 Ok(summaries.abi_encode())
831 }
832}
833
834impl Cheatcode for getDeployment_0Call {
835 fn apply_stateful<CTX: ContextTr>(&self, ccx: &mut CheatsCtxt<'_, CTX>) -> Result {
836 let Self { contractName } = self;
837 let chain_id = ccx.ecx.cfg().chain_id();
838
839 let latest_broadcast = latest_broadcast(
840 contractName,
841 chain_id,
842 &ccx.state.config.broadcast,
843 vec![CallKind::Create, CallKind::Create2],
844 )?;
845
846 Ok(latest_broadcast.contractAddress.abi_encode())
847 }
848}
849
850impl Cheatcode for getDeployment_1Call {
851 fn apply(&self, state: &mut Cheatcodes) -> Result {
852 let Self { contractName, chainId } = self;
853
854 let latest_broadcast = latest_broadcast(
855 contractName,
856 *chainId,
857 &state.config.broadcast,
858 vec![CallKind::Create, CallKind::Create2],
859 )?;
860
861 Ok(latest_broadcast.contractAddress.abi_encode())
862 }
863}
864
865impl Cheatcode for getDeploymentsCall {
866 fn apply(&self, state: &mut Cheatcodes) -> Result {
867 let Self { contractName, chainId } = self;
868
869 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
870 .with_tx_type(CallKind::Create)
871 .with_tx_type(CallKind::Create2);
872
873 let broadcasts = reader.read::<Ethereum>()?;
874
875 let summaries = broadcasts
876 .into_iter()
877 .flat_map(|broadcast| {
878 let results = reader.into_tx_receipts(broadcast);
879 parse_broadcast_results(results)
880 })
881 .collect::<Vec<_>>();
882
883 let deployed_addresses =
884 summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
885
886 Ok(deployed_addresses.abi_encode())
887 }
888}
889
890fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
891 match tx_type {
892 BroadcastTxType::Call => CallKind::Call,
893 BroadcastTxType::Create => CallKind::Create,
894 BroadcastTxType::Create2 => CallKind::Create2,
895 _ => unreachable!("invalid tx type"),
896 }
897}
898
899fn parse_broadcast_results<N: Network>(
900 results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
901) -> Vec<BroadcastTxSummary> {
902 results
903 .into_iter()
904 .map(|(tx, receipt)| BroadcastTxSummary {
905 txHash: receipt.transaction_hash(),
906 blockNumber: receipt.block_number().unwrap_or_default(),
907 txType: match tx.opcode {
908 CallKind::Call => BroadcastTxType::Call,
909 CallKind::Create => BroadcastTxType::Create,
910 CallKind::Create2 => BroadcastTxType::Create2,
911 _ => unreachable!("invalid tx type"),
912 },
913 contractAddress: tx.contract_address.unwrap_or_default(),
914 success: receipt.status(),
915 })
916 .collect()
917}
918
919fn latest_broadcast(
920 contract_name: &String,
921 chain_id: u64,
922 broadcast_path: &Path,
923 filters: Vec<CallKind>,
924) -> Result<BroadcastTxSummary> {
925 let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
926
927 for filter in filters {
928 reader = reader.with_tx_type(filter);
929 }
930
931 let broadcast = reader.read_latest::<Ethereum>()?;
932
933 let results = reader.into_tx_receipts(broadcast);
934
935 let summaries = parse_broadcast_results(results);
936
937 summaries
938 .first()
939 .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
940 .cloned()
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946 use crate::CheatsConfig;
947 use std::sync::Arc;
948
949 fn cheats() -> Cheatcodes {
950 let config = CheatsConfig {
951 ffi: true,
952 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
953 ..Default::default()
954 };
955 Cheatcodes::new(Arc::new(config))
956 }
957
958 #[test]
959 fn test_ffi_hex() {
960 let msg = b"gm";
961 let cheats = cheats();
962 let args = ["echo".to_string(), hex::encode(msg)];
963 let output = ffi(&cheats, &args).unwrap();
964 assert_eq!(output.stdout, Bytes::from(msg));
965 }
966
967 #[test]
968 fn test_ffi_string() {
969 let msg = "gm";
970 let cheats = cheats();
971 let args = ["echo".to_string(), msg.to_string()];
972 let output = ffi(&cheats, &args).unwrap();
973 assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
974 }
975
976 #[test]
977 fn test_ffi_fails_on_error_code() {
978 let mut cheats = cheats();
979
980 #[cfg(unix)]
982 let args = vec!["false".to_string()];
983 #[cfg(windows)]
984 let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
985
986 let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
987
988 assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
990
991 let err_msg = result.unwrap_err().to_string();
993 assert!(
994 err_msg.contains("exited with code 1"),
995 "Error message did not contain exit code: {err_msg}"
996 );
997 }
998
999 #[test]
1000 fn test_artifact_parsing() {
1001 let s = include_str!("../../evm/test-data/solc-obj.json");
1002 let artifact: ContractObject = serde_json::from_str(s).unwrap();
1003 assert!(artifact.bytecode.is_some());
1004
1005 let artifact: ContractObject = serde_json::from_str(s).unwrap();
1006 assert!(artifact.deployed_bytecode.is_some());
1007 }
1008
1009 #[test]
1010 fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1011 let artifact_json = r#"{
1012 "abi": [],
1013 "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1014 }"#;
1015
1016 let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1017 assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1018 let err = result.unwrap_err().to_string();
1019 assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1020 }
1021}