Skip to main content

foundry_cheatcodes/
fs.rs

1//! Implementations of [`Filesystem`](spec::Group::Filesystem) cheatcodes.
2
3use 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        // These fields not available on all platforms; default to 0
49        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        // Get reader for previously opened file to continue reading OR initialize new reader
188        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        // Remove trailing newline character, preserving others for cases where it may be important
197        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        // also remove from the set if opened previously
233        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
401/// Helper function to deploy contract from artifact code.
402/// Uses CREATE2 scheme if salt specified.
403fn 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 active broadcast then set flag to deploy from code.
414    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    // If prank active at current depth, then use it as caller for create input.
426    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
450/// Returns the bytecode from a JSON artifact file.
451///
452/// Can parse following input formats:
453/// - `path/to/artifact.json`
454/// - `path/to/contract.sol`
455/// - `path/to/contract.sol:ContractName`
456/// - `path/to/contract.sol:ContractName:0.8.23`
457/// - `path/to/contract.sol:0.8.23`
458/// - `ContractName`
459/// - `ContractName:0.8.23`
460///
461/// This function is safe to use with contracts that have library dependencies.
462/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
463/// reject artifacts with unlinked library placeholders.
464fn 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        // Use available artifacts list if present
497        if let Some(artifacts) = &state.config.available_artifacts {
498            let filtered = artifacts
499                .iter()
500                .filter(|(id, _)| {
501                    // name might be in the form of "Counter.0.8.23"
502                    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                    // If we know the current script/test contract solc version, try to filter by it
531                    Some(
532                        state
533                            .config
534                            .running_artifact
535                            .as_ref()
536                            .and_then(|running| {
537                                // Firstly filter by version
538                                filtered.retain(|(id, _)| id.version == running.version);
539
540                                // Return artifact if only one matched
541                                if filtered.len() == 1 {
542                                    return Some(filtered[0]);
543                                }
544
545                                // Try filtering by profile as well
546                                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        // Fallback: construct path manually when no artifacts list or no match found
570        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        // Check the exit code of the command.
608        if output.exitCode != 0 {
609            // If the command failed, return an error with the exit code and stderr.
610            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 the command succeeded but still wrote to stderr, log it as a warning.
619        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        // We already hex-decoded the stdout in the `ffi` helper function.
625        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    // write access to foundry.toml is not allowed
674    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    // The stdout might be encoded on valid hex, or it might just be a string,
730    // so we need to determine which it is to avoid improperly encoding later.
731    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        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
981        #[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 that the cheatcode returned an error.
989        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
990
991        // Assert that the error message contains the expected information.
992        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}