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, Result, Vm::*, inspector::exec_create,
6};
7use alloy_dyn_abi::DynSolType;
8use alloy_json_abi::ContractObject;
9use alloy_network::{Network, ReceiptResponse};
10use alloy_primitives::{Bytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::fs;
15use foundry_config::fs_permissions::FsAccessKind;
16use foundry_evm_core::evm::FoundryEvmNetwork;
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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, _state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> 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<FEN: FoundryEvmNetwork>(
315        &self,
316        ccx: &mut CheatsCtxt<'_, '_, FEN>,
317        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
326        &self,
327        ccx: &mut CheatsCtxt<'_, '_, FEN>,
328        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
337        &self,
338        ccx: &mut CheatsCtxt<'_, '_, FEN>,
339        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
348        &self,
349        ccx: &mut CheatsCtxt<'_, '_, FEN>,
350        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
359        &self,
360        ccx: &mut CheatsCtxt<'_, '_, FEN>,
361        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
370        &self,
371        ccx: &mut CheatsCtxt<'_, '_, FEN>,
372        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
381        &self,
382        ccx: &mut CheatsCtxt<'_, '_, FEN>,
383        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
392        &self,
393        ccx: &mut CheatsCtxt<'_, '_, FEN>,
394        executor: &mut dyn CheatcodesExecutor<FEN>,
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<FEN: FoundryEvmNetwork>(
404    ccx: &mut CheatsCtxt<'_, '_, FEN>,
405    executor: &mut dyn CheatcodesExecutor<FEN>,
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            0,
438        ),
439        ccx,
440    )?;
441
442    if !outcome.result.result.is_ok() {
443        return Err(crate::Error::from(outcome.result.output));
444    }
445
446    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
447
448    Ok(address.abi_encode())
449}
450
451/// Returns the bytecode from a JSON artifact file.
452///
453/// Can parse following input formats:
454/// - `path/to/artifact.json`
455/// - `path/to/contract.sol`
456/// - `path/to/contract.sol:ContractName`
457/// - `path/to/contract.sol:ContractName:0.8.23`
458/// - `path/to/contract.sol:0.8.23`
459/// - `ContractName`
460/// - `ContractName:0.8.23`
461///
462/// This function is safe to use with contracts that have library dependencies.
463/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
464/// reject artifacts with unlinked library placeholders.
465fn get_artifact_code<FEN: FoundryEvmNetwork>(
466    state: &Cheatcodes<FEN>,
467    path: &str,
468    deployed: bool,
469) -> Result<Bytes> {
470    let path = if path.ends_with(".json") {
471        PathBuf::from(path)
472    } else {
473        let mut parts = path.split(':');
474
475        let mut file = None;
476        let mut contract_name = None;
477        let mut version = None;
478
479        let path_or_name = parts.next().unwrap();
480        if path_or_name.contains('.') {
481            file = Some(PathBuf::from(path_or_name));
482            if let Some(name_or_version) = parts.next() {
483                if name_or_version.contains('.') {
484                    version = Some(name_or_version);
485                } else {
486                    contract_name = Some(name_or_version);
487                    version = parts.next();
488                }
489            }
490        } else {
491            contract_name = Some(path_or_name);
492            version = parts.next();
493        }
494
495        let version = if let Some(version) = version {
496            Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
497        } else {
498            None
499        };
500
501        // Use available artifacts list if present
502        if let Some(artifacts) = &state.config.available_artifacts {
503            let filtered = artifacts
504                .iter()
505                .filter(|(id, _)| {
506                    // name might be in the form of "Counter.0.8.23"
507                    let id_name = id.name.split('.').next().unwrap();
508
509                    if let Some(path) = &file
510                        && !id.source.ends_with(path)
511                    {
512                        return false;
513                    }
514                    if let Some(name) = contract_name
515                        && id_name != name
516                    {
517                        return false;
518                    }
519                    if let Some(ref version) = version
520                        && (id.version.minor != version.minor
521                            || id.version.major != version.major
522                            || id.version.patch != version.patch)
523                    {
524                        return false;
525                    }
526                    true
527                })
528                .collect::<Vec<_>>();
529
530            let artifact = match &filtered[..] {
531                [] => None,
532                [artifact] => Some(Ok(*artifact)),
533                filtered => {
534                    let mut filtered = filtered.to_vec();
535                    // If we know the current script/test contract solc version, try to filter by it
536                    Some(
537                        state
538                            .config
539                            .running_artifact
540                            .as_ref()
541                            .and_then(|running| {
542                                // Firstly filter by version
543                                filtered.retain(|(id, _)| id.version == running.version);
544
545                                // Return artifact if only one matched
546                                if filtered.len() == 1 {
547                                    return Some(filtered[0]);
548                                }
549
550                                // Try filtering by profile as well
551                                filtered.retain(|(id, _)| id.profile == running.profile);
552
553                                (filtered.len() == 1).then(|| filtered[0])
554                            })
555                            .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
556                    )
557                }
558            };
559
560            if let Some(artifact) = artifact {
561                let artifact = artifact?;
562                let maybe_bytecode = if deployed {
563                    artifact.1.deployed_bytecode().cloned()
564                } else {
565                    artifact.1.bytecode().cloned()
566                };
567
568                return maybe_bytecode.ok_or_else(|| {
569                    fmt_err!("no bytecode for contract; is it abstract or unlinked?")
570                });
571            }
572        }
573
574        // Fallback: construct path manually when no artifacts list or no match found
575        let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name)
576        {
577            (Some(file), Some(contract_name)) => {
578                PathBuf::from(format!("{file}/{contract_name}.json"))
579            }
580            (None, Some(contract_name)) => {
581                PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
582            }
583            (Some(file), None) => {
584                let name = file.replace(".sol", "");
585                PathBuf::from(format!("{file}/{name}.json"))
586            }
587            _ => bail!("invalid artifact path"),
588        };
589
590        state.config.paths.artifacts.join(path_in_artifacts)
591    };
592
593    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
594    let data = fs::read_to_string(path).map_err(|e| {
595        if state.config.available_artifacts.is_some() {
596            fmt_err!("no matching artifact found")
597        } else {
598            e.into()
599        }
600    })?;
601    let artifact = serde_json::from_str::<ContractObject>(&data)?;
602    let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode };
603    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
604}
605
606impl Cheatcode for ffiCall {
607    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
608        let Self { commandInput: input } = self;
609
610        let output = ffi(state, input)?;
611
612        // Check the exit code of the command.
613        if output.exitCode != 0 {
614            // If the command failed, return an error with the exit code and stderr.
615            return Err(fmt_err!(
616                "ffi command {:?} exited with code {}. stderr: {}",
617                input,
618                output.exitCode,
619                String::from_utf8_lossy(&output.stderr)
620            ));
621        }
622
623        // If the command succeeded but still wrote to stderr, log it as a warning.
624        if !output.stderr.is_empty() {
625            let stderr = String::from_utf8_lossy(&output.stderr);
626            warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
627        }
628
629        // We already hex-decoded the stdout in the `ffi` helper function.
630        Ok(output.stdout.abi_encode())
631    }
632}
633
634impl Cheatcode for tryFfiCall {
635    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
636        let Self { commandInput: input } = self;
637        ffi(state, input).map(|res| res.abi_encode())
638    }
639}
640
641impl Cheatcode for promptCall {
642    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
643        let Self { promptText: text } = self;
644        prompt(state, text, prompt_input).map(|res| res.abi_encode())
645    }
646}
647
648impl Cheatcode for promptSecretCall {
649    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
650        let Self { promptText: text } = self;
651        prompt(state, text, prompt_password).map(|res| res.abi_encode())
652    }
653}
654
655impl Cheatcode for promptSecretUintCall {
656    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
657        let Self { promptText: text } = self;
658        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
659    }
660}
661
662impl Cheatcode for promptAddressCall {
663    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
664        let Self { promptText: text } = self;
665        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
666    }
667}
668
669impl Cheatcode for promptUintCall {
670    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
671        let Self { promptText: text } = self;
672        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
673    }
674}
675
676pub(super) fn write_file<FEN: FoundryEvmNetwork>(
677    state: &Cheatcodes<FEN>,
678    path: &Path,
679    contents: &[u8],
680) -> Result {
681    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
682    // write access to foundry.toml is not allowed
683    state.config.ensure_not_foundry_toml(&path)?;
684
685    if state.fs_commit {
686        fs::locked_write(path, contents)?;
687    }
688
689    Ok(Default::default())
690}
691
692fn read_dir<FEN: FoundryEvmNetwork>(
693    state: &Cheatcodes<FEN>,
694    path: &Path,
695    max_depth: u64,
696    follow_links: bool,
697) -> Result {
698    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
699    let paths: Vec<DirEntry> = WalkDir::new(root)
700        .min_depth(1)
701        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
702        .follow_links(follow_links)
703        .contents_first(false)
704        .same_file_system(true)
705        .sort_by_file_name()
706        .into_iter()
707        .map(|entry| match entry {
708            Ok(entry) => DirEntry {
709                errorMessage: String::new(),
710                path: entry.path().display().to_string(),
711                depth: entry.depth() as u64,
712                isDir: entry.file_type().is_dir(),
713                isSymlink: entry.path_is_symlink(),
714            },
715            Err(e) => DirEntry {
716                errorMessage: e.to_string(),
717                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
718                depth: e.depth() as u64,
719                isDir: false,
720                isSymlink: false,
721            },
722        })
723        .collect();
724    Ok(paths.abi_encode())
725}
726
727fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
728    ensure!(
729        state.config.ffi,
730        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
731    );
732    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
733    let mut cmd = Command::new(&input[0]);
734    cmd.args(&input[1..]);
735
736    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
737
738    let output = cmd
739        .current_dir(&state.config.root)
740        .output()
741        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))?;
742
743    // The stdout might be encoded on valid hex, or it might just be a string,
744    // so we need to determine which it is to avoid improperly encoding later.
745    let trimmed_stdout = String::from_utf8(output.stdout)?;
746    let trimmed_stdout = trimmed_stdout.trim();
747    let encoded_stdout = if let Ok(hex) = hex::decode(trimmed_stdout) {
748        hex
749    } else {
750        trimmed_stdout.as_bytes().to_vec()
751    };
752    Ok(FfiResult {
753        exitCode: output.status.code().unwrap_or(69),
754        stdout: encoded_stdout.into(),
755        stderr: output.stderr.into(),
756    })
757}
758
759fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
760    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
761}
762
763fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
764    Password::new().with_prompt(prompt_text).interact()
765}
766
767fn prompt<FEN: FoundryEvmNetwork>(
768    state: &Cheatcodes<FEN>,
769    prompt_text: &str,
770    input: fn(&str) -> Result<String, dialoguer::Error>,
771) -> Result<String> {
772    let text_clone = prompt_text.to_string();
773    let timeout = state.config.prompt_timeout;
774    let (tx, rx) = mpsc::channel();
775
776    thread::spawn(move || {
777        let _ = tx.send(input(&text_clone));
778    });
779
780    match rx.recv_timeout(timeout) {
781        Ok(res) => res.map_err(|err| {
782            let _ = sh_println!();
783            err.to_string().into()
784        }),
785        Err(_) => {
786            let _ = sh_eprintln!();
787            Err("Prompt timed out".into())
788        }
789    }
790}
791
792impl Cheatcode for getBroadcastCall {
793    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
794        let Self { contractName, chainId, txType } = self;
795
796        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
797            contractName,
798            *chainId,
799            &state.config.broadcast,
800            vec![map_broadcast_tx_type(*txType)],
801        )?;
802
803        Ok(latest_broadcast.abi_encode())
804    }
805}
806
807impl Cheatcode for getBroadcasts_0Call {
808    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
809        let Self { contractName, chainId, txType } = self;
810
811        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
812            .with_tx_type(map_broadcast_tx_type(*txType));
813
814        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
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        Ok(summaries.abi_encode())
825    }
826}
827
828impl Cheatcode for getBroadcasts_1Call {
829    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
830        let Self { contractName, chainId } = self;
831
832        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
833
834        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
835
836        let summaries = broadcasts
837            .into_iter()
838            .flat_map(|broadcast| {
839                let results = reader.into_tx_receipts(broadcast);
840                parse_broadcast_results(results)
841            })
842            .collect::<Vec<_>>();
843
844        Ok(summaries.abi_encode())
845    }
846}
847
848impl Cheatcode for getDeployment_0Call {
849    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
850        let Self { contractName } = self;
851        let chain_id = ccx.ecx.cfg().chain_id();
852
853        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
854            contractName,
855            chain_id,
856            &ccx.state.config.broadcast,
857            vec![CallKind::Create, CallKind::Create2],
858        )?;
859
860        Ok(latest_broadcast.contractAddress.abi_encode())
861    }
862}
863
864impl Cheatcode for getDeployment_1Call {
865    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
866        let Self { contractName, chainId } = self;
867
868        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
869            contractName,
870            *chainId,
871            &state.config.broadcast,
872            vec![CallKind::Create, CallKind::Create2],
873        )?;
874
875        Ok(latest_broadcast.contractAddress.abi_encode())
876    }
877}
878
879impl Cheatcode for getDeploymentsCall {
880    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
881        let Self { contractName, chainId } = self;
882
883        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
884            .with_tx_type(CallKind::Create)
885            .with_tx_type(CallKind::Create2);
886
887        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
888
889        let summaries = broadcasts
890            .into_iter()
891            .flat_map(|broadcast| {
892                let results = reader.into_tx_receipts(broadcast);
893                parse_broadcast_results(results)
894            })
895            .collect::<Vec<_>>();
896
897        let deployed_addresses =
898            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
899
900        Ok(deployed_addresses.abi_encode())
901    }
902}
903
904fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
905    match tx_type {
906        BroadcastTxType::Call => CallKind::Call,
907        BroadcastTxType::Create => CallKind::Create,
908        BroadcastTxType::Create2 => CallKind::Create2,
909        _ => unreachable!("invalid tx type"),
910    }
911}
912
913fn parse_broadcast_results<N: Network>(
914    results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
915) -> Vec<BroadcastTxSummary> {
916    results
917        .into_iter()
918        .map(|(tx, receipt)| BroadcastTxSummary {
919            txHash: receipt.transaction_hash(),
920            blockNumber: receipt.block_number().unwrap_or_default(),
921            txType: match tx.call_kind {
922                CallKind::Call => BroadcastTxType::Call,
923                CallKind::Create => BroadcastTxType::Create,
924                CallKind::Create2 => BroadcastTxType::Create2,
925                _ => unreachable!("invalid tx type"),
926            },
927            contractAddress: tx.contract_address.unwrap_or_default(),
928            success: receipt.status(),
929        })
930        .collect()
931}
932
933fn latest_broadcast<N: Network>(
934    contract_name: &String,
935    chain_id: u64,
936    broadcast_path: &Path,
937    filters: Vec<CallKind>,
938) -> Result<BroadcastTxSummary>
939where
940    N::TxEnvelope: for<'d> serde::Deserialize<'d>,
941{
942    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
943
944    for filter in filters {
945        reader = reader.with_tx_type(filter);
946    }
947
948    let broadcast = reader.read_latest::<N>()?;
949
950    let results = reader.into_tx_receipts(broadcast);
951
952    let summaries = parse_broadcast_results(results);
953
954    summaries
955        .first()
956        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
957        .cloned()
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963    use crate::CheatsConfig;
964    use alloy_primitives::{address, b256};
965    use foundry_evm_core::evm::TempoEvmNetwork;
966    use std::{env, fs as stdfs, sync::Arc};
967
968    fn cheats() -> Cheatcodes {
969        let config = CheatsConfig {
970            ffi: true,
971            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
972            ..Default::default()
973        };
974        Cheatcodes::new(Arc::new(config))
975    }
976
977    #[test]
978    fn test_ffi_hex() {
979        let msg = b"gm";
980        let cheats = cheats();
981        let args = ["echo".to_string(), hex::encode(msg)];
982        let output = ffi(&cheats, &args).unwrap();
983        assert_eq!(output.stdout, Bytes::from(msg));
984    }
985
986    #[test]
987    fn test_ffi_string() {
988        let msg = "gm";
989        let cheats = cheats();
990        let args = ["echo".to_string(), msg.to_string()];
991        let output = ffi(&cheats, &args).unwrap();
992        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
993    }
994
995    #[test]
996    fn test_ffi_fails_on_error_code() {
997        let mut cheats = cheats();
998
999        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
1000        #[cfg(unix)]
1001        let args = vec!["false".to_string()];
1002        #[cfg(windows)]
1003        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1004
1005        let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1006
1007        // Assert that the cheatcode returned an error.
1008        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1009
1010        // Assert that the error message contains the expected information.
1011        let err_msg = result.unwrap_err().to_string();
1012        assert!(
1013            err_msg.contains("exited with code 1"),
1014            "Error message did not contain exit code: {err_msg}"
1015        );
1016    }
1017
1018    #[test]
1019    fn test_artifact_parsing() {
1020        let s = include_str!("../../evm/test-data/solc-obj.json");
1021        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1022        assert!(artifact.bytecode.is_some());
1023
1024        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1025        assert!(artifact.deployed_bytecode.is_some());
1026    }
1027
1028    #[test]
1029    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1030        let artifact_json = r#"{
1031            "abi": [],
1032            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1033        }"#;
1034
1035        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1036        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1037        let err = result.unwrap_err().to_string();
1038        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1039    }
1040
1041    fn unique_temp_dir(prefix: &str) -> PathBuf {
1042        env::temp_dir().join(format!(
1043            "foundry-cheatcodes-{prefix}-{}",
1044            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1045        ))
1046    }
1047
1048    #[test]
1049    fn test_latest_broadcast_reads_tempo_sequences() {
1050        let root = unique_temp_dir("tempo-broadcast");
1051        let broadcast_path = root.join("broadcast");
1052        let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1053        stdfs::create_dir_all(&sequence_dir).unwrap();
1054
1055        let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1056        let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1057        let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1058        let contract_address = "0x20c0000000000000000000000000000000000000";
1059        let zero_bloom = format!("0x{}", "0".repeat(512));
1060
1061        let sequence = serde_json::json!({
1062            "transactions": [{
1063                "hash": tx_hash,
1064                "transactionType": "CREATE",
1065                "contractName": "Counter",
1066                "contractAddress": contract_address,
1067                "function": serde_json::Value::Null,
1068                "arguments": serde_json::Value::Null,
1069                "transaction": {
1070                    "type": "0x76",
1071                    "from": from,
1072                    "to": serde_json::Value::Null,
1073                    "data": "0x",
1074                    "value": "0x0",
1075                    "gas": "0x5208",
1076                    "nonce": "0x0",
1077                    "accessList": [],
1078                    "calls": [],
1079                    "nonceKey": "0x0",
1080                    "feePayerSignature": serde_json::Value::Null,
1081                    "validBefore": serde_json::Value::Null,
1082                    "validAfter": serde_json::Value::Null,
1083                    "keyAuthorization": serde_json::Value::Null,
1084                    "aaAuthorizationList": []
1085                },
1086                "additionalContracts": [],
1087                "isFixedGasLimit": false
1088            }],
1089            "receipts": [{
1090                "type": "0x76",
1091                "status": "0x1",
1092                "cumulativeGasUsed": "0x5208",
1093                "logs": [],
1094                "logsBloom": zero_bloom,
1095                "transactionHash": tx_hash,
1096                "transactionIndex": "0x0",
1097                "blockHash": block_hash,
1098                "blockNumber": "0x7",
1099                "gasUsed": "0x5208",
1100                "effectiveGasPrice": "0x1",
1101                "from": from,
1102                "to": serde_json::Value::Null,
1103                "contractAddress": contract_address,
1104                "feePayer": from
1105            }],
1106            "libraries": [],
1107            "pending": [],
1108            "returns": {},
1109            "timestamp": 1,
1110            "chain": 31337,
1111            "commit": serde_json::Value::Null
1112        });
1113
1114        fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1115
1116        let latest = latest_broadcast::<<TempoEvmNetwork as FoundryEvmNetwork>::Network>(
1117            &"Counter".to_owned(),
1118            31337,
1119            &broadcast_path,
1120            vec![CallKind::Create],
1121        )
1122        .unwrap();
1123
1124        assert_eq!(
1125            latest.txHash,
1126            b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1127        );
1128        assert_eq!(latest.blockNumber, 7);
1129        assert!(matches!(latest.txType, BroadcastTxType::Create));
1130        assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1131        assert!(latest.success);
1132
1133        stdfs::remove_dir_all(root).unwrap();
1134    }
1135}