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::{ContextTr, 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 =
380 ccx.state.get_prank(ccx.ecx.journal().depth()).map_or(ccx.caller, |prank| prank.new_caller);
381
382 let outcome = executor.exec_create(
383 CreateInputs::new(
384 caller,
385 scheme,
386 value.unwrap_or(U256::ZERO),
387 bytecode.into(),
388 ccx.gas_limit,
389 ),
390 ccx,
391 )?;
392
393 if !outcome.result.result.is_ok() {
394 return Err(crate::Error::from(outcome.result.output));
395 }
396
397 let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
398
399 Ok(address.abi_encode())
400}
401
402fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result<Bytes> {
417 let path = if path.ends_with(".json") {
418 PathBuf::from(path)
419 } else {
420 let mut parts = path.split(':');
421
422 let mut file = None;
423 let mut contract_name = None;
424 let mut version = None;
425
426 let path_or_name = parts.next().unwrap();
427 if path_or_name.contains('.') {
428 file = Some(PathBuf::from(path_or_name));
429 if let Some(name_or_version) = parts.next() {
430 if name_or_version.contains('.') {
431 version = Some(name_or_version);
432 } else {
433 contract_name = Some(name_or_version);
434 version = parts.next();
435 }
436 }
437 } else {
438 contract_name = Some(path_or_name);
439 version = parts.next();
440 }
441
442 let version = if let Some(version) = version {
443 Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
444 } else {
445 None
446 };
447
448 if let Some(artifacts) = &state.config.available_artifacts {
450 let filtered = artifacts
451 .iter()
452 .filter(|(id, _)| {
453 let id_name = id.name.split('.').next().unwrap();
455
456 if let Some(path) = &file
457 && !id.source.ends_with(path)
458 {
459 return false;
460 }
461 if let Some(name) = contract_name
462 && id_name != name
463 {
464 return false;
465 }
466 if let Some(ref version) = version
467 && (id.version.minor != version.minor
468 || id.version.major != version.major
469 || id.version.patch != version.patch)
470 {
471 return false;
472 }
473 true
474 })
475 .collect::<Vec<_>>();
476
477 let artifact = match &filtered[..] {
478 [] => None,
479 [artifact] => Some(Ok(*artifact)),
480 filtered => {
481 let mut filtered = filtered.to_vec();
482 Some(
484 state
485 .config
486 .running_artifact
487 .as_ref()
488 .and_then(|running| {
489 filtered.retain(|(id, _)| id.version == running.version);
491
492 if filtered.len() == 1 {
494 return Some(filtered[0]);
495 }
496
497 filtered.retain(|(id, _)| id.profile == running.profile);
499
500 if filtered.len() == 1 { Some(filtered[0]) } else { None }
501 })
502 .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
503 )
504 }
505 };
506
507 if let Some(artifact) = artifact {
508 let artifact = artifact?;
509 let maybe_bytecode = if deployed {
510 artifact.1.deployed_bytecode().cloned()
511 } else {
512 artifact.1.bytecode().cloned()
513 };
514
515 return maybe_bytecode.ok_or_else(|| {
516 fmt_err!("no bytecode for contract; is it abstract or unlinked?")
517 });
518 }
519 }
520
521 let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name)
523 {
524 (Some(file), Some(contract_name)) => {
525 PathBuf::from(format!("{file}/{contract_name}.json"))
526 }
527 (None, Some(contract_name)) => {
528 PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
529 }
530 (Some(file), None) => {
531 let name = file.replace(".sol", "");
532 PathBuf::from(format!("{file}/{name}.json"))
533 }
534 _ => bail!("invalid artifact path"),
535 };
536
537 state.config.paths.artifacts.join(path_in_artifacts)
538 };
539
540 let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
541 let data = fs::read_to_string(path).map_err(|e| {
542 if state.config.available_artifacts.is_some() {
543 fmt_err!("no matching artifact found")
544 } else {
545 e.into()
546 }
547 })?;
548 let artifact = serde_json::from_str::<ContractObject>(&data)?;
549 let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
550 maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
551}
552
553impl Cheatcode for ffiCall {
554 fn apply(&self, state: &mut Cheatcodes) -> Result {
555 let Self { commandInput: input } = self;
556
557 let output = ffi(state, input)?;
558
559 if output.exitCode != 0 {
561 return Err(fmt_err!(
563 "ffi command {:?} exited with code {}. stderr: {}",
564 input,
565 output.exitCode,
566 String::from_utf8_lossy(&output.stderr)
567 ));
568 }
569
570 if !output.stderr.is_empty() {
572 let stderr = String::from_utf8_lossy(&output.stderr);
573 warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
574 }
575
576 Ok(output.stdout.abi_encode())
578 }
579}
580
581impl Cheatcode for tryFfiCall {
582 fn apply(&self, state: &mut Cheatcodes) -> Result {
583 let Self { commandInput: input } = self;
584 ffi(state, input).map(|res| res.abi_encode())
585 }
586}
587
588impl Cheatcode for promptCall {
589 fn apply(&self, state: &mut Cheatcodes) -> Result {
590 let Self { promptText: text } = self;
591 prompt(state, text, prompt_input).map(|res| res.abi_encode())
592 }
593}
594
595impl Cheatcode for promptSecretCall {
596 fn apply(&self, state: &mut Cheatcodes) -> Result {
597 let Self { promptText: text } = self;
598 prompt(state, text, prompt_password).map(|res| res.abi_encode())
599 }
600}
601
602impl Cheatcode for promptSecretUintCall {
603 fn apply(&self, state: &mut Cheatcodes) -> Result {
604 let Self { promptText: text } = self;
605 parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
606 }
607}
608
609impl Cheatcode for promptAddressCall {
610 fn apply(&self, state: &mut Cheatcodes) -> Result {
611 let Self { promptText: text } = self;
612 parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
613 }
614}
615
616impl Cheatcode for promptUintCall {
617 fn apply(&self, state: &mut Cheatcodes) -> Result {
618 let Self { promptText: text } = self;
619 parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
620 }
621}
622
623pub(super) fn write_file(state: &Cheatcodes, path: &Path, contents: &[u8]) -> Result {
624 let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
625 state.config.ensure_not_foundry_toml(&path)?;
627
628 if state.fs_commit {
629 fs::locked_write(path, contents)?;
630 }
631
632 Ok(Default::default())
633}
634
635fn read_dir(state: &Cheatcodes, path: &Path, max_depth: u64, follow_links: bool) -> Result {
636 let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
637 let paths: Vec<DirEntry> = WalkDir::new(root)
638 .min_depth(1)
639 .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
640 .follow_links(follow_links)
641 .contents_first(false)
642 .same_file_system(true)
643 .sort_by_file_name()
644 .into_iter()
645 .map(|entry| match entry {
646 Ok(entry) => DirEntry {
647 errorMessage: String::new(),
648 path: entry.path().display().to_string(),
649 depth: entry.depth() as u64,
650 isDir: entry.file_type().is_dir(),
651 isSymlink: entry.path_is_symlink(),
652 },
653 Err(e) => DirEntry {
654 errorMessage: e.to_string(),
655 path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
656 depth: e.depth() as u64,
657 isDir: false,
658 isSymlink: false,
659 },
660 })
661 .collect();
662 Ok(paths.abi_encode())
663}
664
665fn ffi(state: &Cheatcodes, input: &[String]) -> Result<FfiResult> {
666 ensure!(
667 state.config.ffi,
668 "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
669 );
670 ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
671 let mut cmd = Command::new(&input[0]);
672 cmd.args(&input[1..]);
673
674 debug!(target: "cheatcodes", ?cmd, "invoking ffi");
675
676 let output = cmd
677 .current_dir(&state.config.root)
678 .output()
679 .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
680
681 let trimmed_stdout = String::from_utf8(output.stdout)?;
684 let trimmed_stdout = trimmed_stdout.trim();
685 let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
686 hex
687 } else {
688 trimmed_stdout.as_bytes().to_vec()
689 };
690 Ok(FfiResult {
691 exitCode: output.status.code().unwrap_or(69),
692 stdout: encoded_stdout.into(),
693 stderr: output.stderr.into(),
694 })
695}
696
697fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
698 Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
699}
700
701fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
702 Password::new().with_prompt(prompt_text).interact()
703}
704
705fn prompt(
706 state: &Cheatcodes,
707 prompt_text: &str,
708 input: fn(&str) -> Result<String, dialoguer::Error>,
709) -> Result<String> {
710 let text_clone = prompt_text.to_string();
711 let timeout = state.config.prompt_timeout;
712 let (tx, rx) = mpsc::channel();
713
714 thread::spawn(move || {
715 let _ = tx.send(input(&text_clone));
716 });
717
718 match rx.recv_timeout(timeout) {
719 Ok(res) => res.map_err(|err| {
720 let _ = sh_println!();
721 err.to_string().into()
722 }),
723 Err(_) => {
724 let _ = sh_eprintln!();
725 Err("Prompt timed out".into())
726 }
727 }
728}
729
730impl Cheatcode for getBroadcastCall {
731 fn apply(&self, state: &mut Cheatcodes) -> Result {
732 let Self { contractName, chainId, txType } = self;
733
734 let latest_broadcast = latest_broadcast(
735 contractName,
736 *chainId,
737 &state.config.broadcast,
738 vec![map_broadcast_tx_type(*txType)],
739 )?;
740
741 Ok(latest_broadcast.abi_encode())
742 }
743}
744
745impl Cheatcode for getBroadcasts_0Call {
746 fn apply(&self, state: &mut Cheatcodes) -> Result {
747 let Self { contractName, chainId, txType } = self;
748
749 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
750 .with_tx_type(map_broadcast_tx_type(*txType));
751
752 let broadcasts = reader.read()?;
753
754 let summaries = broadcasts
755 .into_iter()
756 .flat_map(|broadcast| {
757 let results = reader.into_tx_receipts(broadcast);
758 parse_broadcast_results(results)
759 })
760 .collect::<Vec<_>>();
761
762 Ok(summaries.abi_encode())
763 }
764}
765
766impl Cheatcode for getBroadcasts_1Call {
767 fn apply(&self, state: &mut Cheatcodes) -> Result {
768 let Self { contractName, chainId } = self;
769
770 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
771
772 let broadcasts = reader.read()?;
773
774 let summaries = broadcasts
775 .into_iter()
776 .flat_map(|broadcast| {
777 let results = reader.into_tx_receipts(broadcast);
778 parse_broadcast_results(results)
779 })
780 .collect::<Vec<_>>();
781
782 Ok(summaries.abi_encode())
783 }
784}
785
786impl Cheatcode for getDeployment_0Call {
787 fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
788 let Self { contractName } = self;
789 let chain_id = ccx.ecx.cfg().chain_id;
790
791 let latest_broadcast = latest_broadcast(
792 contractName,
793 chain_id,
794 &ccx.state.config.broadcast,
795 vec![CallKind::Create, CallKind::Create2],
796 )?;
797
798 Ok(latest_broadcast.contractAddress.abi_encode())
799 }
800}
801
802impl Cheatcode for getDeployment_1Call {
803 fn apply(&self, state: &mut Cheatcodes) -> Result {
804 let Self { contractName, chainId } = self;
805
806 let latest_broadcast = latest_broadcast(
807 contractName,
808 *chainId,
809 &state.config.broadcast,
810 vec![CallKind::Create, CallKind::Create2],
811 )?;
812
813 Ok(latest_broadcast.contractAddress.abi_encode())
814 }
815}
816
817impl Cheatcode for getDeploymentsCall {
818 fn apply(&self, state: &mut Cheatcodes) -> Result {
819 let Self { contractName, chainId } = self;
820
821 let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
822 .with_tx_type(CallKind::Create)
823 .with_tx_type(CallKind::Create2);
824
825 let broadcasts = reader.read()?;
826
827 let summaries = broadcasts
828 .into_iter()
829 .flat_map(|broadcast| {
830 let results = reader.into_tx_receipts(broadcast);
831 parse_broadcast_results(results)
832 })
833 .collect::<Vec<_>>();
834
835 let deployed_addresses =
836 summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
837
838 Ok(deployed_addresses.abi_encode())
839 }
840}
841
842fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
843 match tx_type {
844 BroadcastTxType::Call => CallKind::Call,
845 BroadcastTxType::Create => CallKind::Create,
846 BroadcastTxType::Create2 => CallKind::Create2,
847 _ => unreachable!("invalid tx type"),
848 }
849}
850
851fn parse_broadcast_results(
852 results: Vec<(TransactionWithMetadata, AnyTransactionReceipt)>,
853) -> Vec<BroadcastTxSummary> {
854 results
855 .into_iter()
856 .map(|(tx, receipt)| BroadcastTxSummary {
857 txHash: receipt.transaction_hash,
858 blockNumber: receipt.block_number.unwrap_or_default(),
859 txType: match tx.opcode {
860 CallKind::Call => BroadcastTxType::Call,
861 CallKind::Create => BroadcastTxType::Create,
862 CallKind::Create2 => BroadcastTxType::Create2,
863 _ => unreachable!("invalid tx type"),
864 },
865 contractAddress: tx.contract_address.unwrap_or_default(),
866 success: receipt.status(),
867 })
868 .collect()
869}
870
871fn latest_broadcast(
872 contract_name: &String,
873 chain_id: u64,
874 broadcast_path: &Path,
875 filters: Vec<CallKind>,
876) -> Result<BroadcastTxSummary> {
877 let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
878
879 for filter in filters {
880 reader = reader.with_tx_type(filter);
881 }
882
883 let broadcast = reader.read_latest()?;
884
885 let results = reader.into_tx_receipts(broadcast);
886
887 let summaries = parse_broadcast_results(results);
888
889 summaries
890 .first()
891 .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
892 .cloned()
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898 use crate::CheatsConfig;
899 use std::sync::Arc;
900
901 fn cheats() -> Cheatcodes {
902 let config = CheatsConfig {
903 ffi: true,
904 root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
905 ..Default::default()
906 };
907 Cheatcodes::new(Arc::new(config))
908 }
909
910 #[test]
911 fn test_ffi_hex() {
912 let msg = b"gm";
913 let cheats = cheats();
914 let args = ["echo".to_string(), hex::encode(msg)];
915 let output = ffi(&cheats, &args).unwrap();
916 assert_eq!(output.stdout, Bytes::from(msg));
917 }
918
919 #[test]
920 fn test_ffi_string() {
921 let msg = "gm";
922 let cheats = cheats();
923 let args = ["echo".to_string(), msg.to_string()];
924 let output = ffi(&cheats, &args).unwrap();
925 assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
926 }
927
928 #[test]
929 fn test_ffi_fails_on_error_code() {
930 let mut cheats = cheats();
931
932 #[cfg(unix)]
934 let args = vec!["false".to_string()];
935 #[cfg(windows)]
936 let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
937
938 let result = ffiCall { commandInput: args }.apply(&mut cheats);
939
940 assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
942
943 let err_msg = result.unwrap_err().to_string();
945 assert!(
946 err_msg.contains("exited with code 1"),
947 "Error message did not contain exit code: {err_msg}"
948 );
949 }
950
951 #[test]
952 fn test_artifact_parsing() {
953 let s = include_str!("../../evm/test-data/solc-obj.json");
954 let artifact: ContractObject = serde_json::from_str(s).unwrap();
955 assert!(artifact.bytecode.is_some());
956
957 let artifact: ContractObject = serde_json::from_str(s).unwrap();
958 assert!(artifact.deployed_bytecode.is_some());
959 }
960
961 #[test]
962 fn test_alloy_json_abi_rejects_unlinked_bytecode() {
963 let artifact_json = r#"{
964 "abi": [],
965 "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
966 }"#;
967
968 let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
969 assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
970 let err = result.unwrap_err().to_string();
971 assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
972 }
973}