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