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