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