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, FixedBytes, U256, hex, map::Entry};
11use alloy_sol_types::SolValue;
12use dialoguer::{Input, Password};
13use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
14use foundry_common::{contracts::ContractData, fs};
15use foundry_config::fs_permissions::FsAccessKind;
16use foundry_evm_core::{FoundryTransaction, env::FoundryContextExt, evm::FoundryEvmNetwork};
17use foundry_evm_traces::CallKind;
18use revm::{context::CreateScheme, interpreter::CreateInputs};
19use semver::Version;
20use std::{
21    io::{BufRead, BufReader},
22    path::{Path, PathBuf},
23    process::Command,
24    sync::mpsc,
25    thread,
26    time::{SystemTime, UNIX_EPOCH},
27};
28use walkdir::WalkDir;
29
30/// Parsed artifact path components.
31#[derive(Debug, Default, PartialEq, Eq)]
32struct ParsedArtifactPath<'a> {
33    file: Option<PathBuf>,
34    contract_name: Option<&'a str>,
35    version: Option<Version>,
36    profile: Option<&'a str>,
37}
38
39/// Parses an artifact path string into its components.
40///
41/// Supports the following formats:
42/// - `path/to/contract.sol`
43/// - `path/to/contract.sol:ContractName`
44/// - `path/to/contract.sol:ContractName:0.8.23`
45/// - `path/to/contract.sol:ContractName:profile`
46/// - `path/to/contract.sol:0.8.23`
47/// - `path/to/contract.sol:profile`
48/// - `ContractName`
49/// - `ContractName:0.8.23`
50/// - `ContractName:profile`
51fn parse_artifact_path(path: &str) -> std::result::Result<ParsedArtifactPath<'_>, String> {
52    // A Windows drive separator belongs to the file, not the artifact's suffix fields.
53    // Recognize it on every host so parsing does not depend on the current platform.
54    let unprefixed = path.strip_prefix(r"\\?\").unwrap_or(path);
55    let drive_prefix_len = match unprefixed.as_bytes() {
56        [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic() => {
57            path.len() - unprefixed.len() + 2
58        }
59        _ => 0,
60    };
61    let mut parts = path[drive_prefix_len..].split(':');
62
63    let mut file = None;
64    let mut contract_name = None;
65    let mut version = None;
66    let mut profile = None;
67
68    let path_or_name = parts.next().unwrap();
69    let path_or_name = &path[..drive_prefix_len + path_or_name.len()];
70    if path_or_name.contains('.') {
71        file = Some(PathBuf::from(path_or_name));
72        if let Some(name_or_version_or_profile) = parts.next() {
73            if name_or_version_or_profile.contains('.')
74                || Version::parse(name_or_version_or_profile).is_ok()
75            {
76                version = Some(name_or_version_or_profile);
77            } else {
78                contract_name = Some(name_or_version_or_profile);
79                if let Some(version_or_profile) = parts.next() {
80                    if version_or_profile.contains('.')
81                        || Version::parse(version_or_profile).is_ok()
82                    {
83                        version = Some(version_or_profile);
84                    } else {
85                        profile = Some(version_or_profile);
86                    }
87                }
88            }
89        }
90    } else {
91        contract_name = Some(path_or_name);
92        if let Some(version_or_profile) = parts.next() {
93            if version_or_profile.contains('.') || Version::parse(version_or_profile).is_ok() {
94                version = Some(version_or_profile);
95            } else {
96                profile = Some(version_or_profile);
97            }
98        }
99    }
100
101    let version = if let Some(version) = version {
102        Some(Version::parse(version).map_err(|e| format!("failed parsing version: {e}"))?)
103    } else {
104        None
105    };
106
107    Ok(ParsedArtifactPath { file, contract_name, version, profile })
108}
109
110impl Cheatcode for existsCall {
111    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
112        let Self { path } = self;
113        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
114        Ok(path.exists().abi_encode())
115    }
116}
117
118impl Cheatcode for fsMetadataCall {
119    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
120        let Self { path } = self;
121        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
122
123        let metadata = path.metadata()?;
124
125        // These fields not available on all platforms; default to 0
126        let [modified, accessed, created] =
127            [metadata.modified(), metadata.accessed(), metadata.created()].map(|time| {
128                time.unwrap_or(UNIX_EPOCH).duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
129            });
130
131        Ok(FsMetadata {
132            isDir: metadata.is_dir(),
133            isSymlink: metadata.is_symlink(),
134            length: U256::from(metadata.len()),
135            readOnly: metadata.permissions().readonly(),
136            modified: U256::from(modified),
137            accessed: U256::from(accessed),
138            created: U256::from(created),
139        }
140        .abi_encode())
141    }
142}
143
144impl Cheatcode for isDirCall {
145    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
146        let Self { path } = self;
147        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
148        Ok(path.is_dir().abi_encode())
149    }
150}
151
152impl Cheatcode for isFileCall {
153    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
154        let Self { path } = self;
155        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
156        Ok(path.is_file().abi_encode())
157    }
158}
159
160impl Cheatcode for projectRootCall {
161    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
162        let Self {} = self;
163        Ok(state.config.root.display().to_string().abi_encode())
164    }
165}
166
167impl Cheatcode for currentFilePathCall {
168    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
169        let Self {} = self;
170        let artifact = state
171            .config
172            .running_artifact
173            .as_ref()
174            .ok_or_else(|| fmt_err!("no running contract found"))?;
175        let relative = artifact.source.strip_prefix(&state.config.root).unwrap_or(&artifact.source);
176        Ok(relative.display().to_string().abi_encode())
177    }
178}
179
180impl Cheatcode for unixTimeCall {
181    fn apply<FEN: FoundryEvmNetwork>(&self, _state: &mut Cheatcodes<FEN>) -> Result {
182        let Self {} = self;
183        let difference = SystemTime::now()
184            .duration_since(UNIX_EPOCH)
185            .map_err(|e| fmt_err!("failed getting Unix timestamp: {e}"))?;
186        Ok(difference.as_millis().abi_encode())
187    }
188}
189
190impl Cheatcode for closeFileCall {
191    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
192        let Self { path } = self;
193        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
194
195        state.test_context.opened_read_files.remove(&path);
196
197        Ok(Default::default())
198    }
199}
200
201impl Cheatcode for copyFileCall {
202    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
203        let Self { from, to } = self;
204        let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?;
205        let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?;
206        state.config.ensure_not_foundry_toml(&to)?;
207
208        let n = fs::copy(from, to)?;
209        Ok(n.abi_encode())
210    }
211}
212
213impl Cheatcode for createDirCall {
214    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
215        let Self { path, recursive } = self;
216        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
217        if *recursive { fs::create_dir_all(path) } else { fs::create_dir(path) }?;
218        Ok(Default::default())
219    }
220}
221
222impl Cheatcode for readDir_0Call {
223    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
224        let Self { path } = self;
225        read_dir(state, path.as_ref(), 1, false)
226    }
227}
228
229impl Cheatcode for readDir_1Call {
230    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
231        let Self { path, maxDepth } = self;
232        read_dir(state, path.as_ref(), *maxDepth, false)
233    }
234}
235
236impl Cheatcode for readDir_2Call {
237    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
238        let Self { path, maxDepth, followLinks } = self;
239        read_dir(state, path.as_ref(), *maxDepth, *followLinks)
240    }
241}
242
243impl Cheatcode for readFileCall {
244    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
245        let Self { path } = self;
246        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
247        Ok(fs::locked_read_to_string(path)?.abi_encode())
248    }
249}
250
251impl Cheatcode for readFileBinaryCall {
252    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
253        let Self { path } = self;
254        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
255        Ok(fs::locked_read(path)?.abi_encode())
256    }
257}
258
259impl Cheatcode for readLineCall {
260    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
261        let Self { path } = self;
262        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
263
264        // Get reader for previously opened file to continue reading OR initialize new reader
265        let reader = match state.test_context.opened_read_files.entry(path.clone()) {
266            Entry::Occupied(entry) => entry.into_mut(),
267            Entry::Vacant(entry) => entry.insert(BufReader::new(fs::open(path)?)),
268        };
269
270        let mut line: String = String::new();
271        reader.read_line(&mut line)?;
272
273        // Remove trailing newline character, preserving others for cases where it may be important
274        if line.ends_with('\n') {
275            line.pop();
276            if line.ends_with('\r') {
277                line.pop();
278            }
279        }
280
281        Ok(line.abi_encode())
282    }
283}
284
285impl Cheatcode for readLinkCall {
286    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
287        let Self { linkPath: path } = self;
288        // Validate the resolved target, but keep the link itself for read_link.
289        state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
290        let target = fs::read_link(state.config.root.join(path))?;
291        Ok(target.display().to_string().abi_encode())
292    }
293}
294
295impl Cheatcode for removeDirCall {
296    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
297        let Self { path, recursive } = self;
298        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
299        state.config.ensure_not_foundry_toml(&path)?;
300        if *recursive { fs::remove_dir_all(path) } else { fs::remove_dir(path) }?;
301        Ok(Default::default())
302    }
303}
304
305impl Cheatcode for removeFileCall {
306    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
307        let Self { path } = self;
308        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
309        state.config.ensure_not_foundry_toml(&path)?;
310
311        // also remove from the set if opened previously
312        state.test_context.opened_read_files.remove(&path);
313
314        if state.fs_commit {
315            fs::remove_file(&path)?;
316        }
317
318        Ok(Default::default())
319    }
320}
321
322impl Cheatcode for writeFileCall {
323    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
324        let Self { path, data } = self;
325        write_file(state, path.as_ref(), data.as_bytes())
326    }
327}
328
329impl Cheatcode for writeFileBinaryCall {
330    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
331        let Self { path, data } = self;
332        write_file(state, path.as_ref(), data)
333    }
334}
335
336impl Cheatcode for writeLineCall {
337    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
338        let Self { path, data: line } = self;
339        let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
340        state.config.ensure_not_foundry_toml(&path)?;
341
342        if state.fs_commit {
343            fs::locked_write_line(path, line)?;
344        }
345
346        Ok(Default::default())
347    }
348}
349
350impl Cheatcode for getArtifactPathByCodeCall {
351    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
352        let Self { code } = self;
353        let (artifact_id, _) = state
354            .config
355            .available_artifacts
356            .as_ref()
357            .and_then(|artifacts| artifacts.find_by_creation_code(code))
358            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
359
360        Ok(artifact_id.path.to_string_lossy().abi_encode())
361    }
362}
363
364impl Cheatcode for getArtifactPathByDeployedCodeCall {
365    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
366        let Self { deployedCode } = self;
367        let (artifact_id, _) = state
368            .config
369            .available_artifacts
370            .as_ref()
371            .and_then(|artifacts| artifacts.find_by_deployed_code(deployedCode))
372            .ok_or_else(|| fmt_err!("no matching artifact found"))?;
373
374        Ok(artifact_id.path.to_string_lossy().abi_encode())
375    }
376}
377
378impl Cheatcode for getCodeCall {
379    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
380        let Self { artifactPath: path } = self;
381        Ok(get_artifact_code(state, path, false)?.abi_encode())
382    }
383}
384
385impl Cheatcode for getDeployedCodeCall {
386    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
387        let Self { artifactPath: path } = self;
388        Ok(get_artifact_code(state, path, true)?.abi_encode())
389    }
390}
391
392impl Cheatcode for getSelectorsCall {
393    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
394        let Self { artifactPath: path } = self;
395        let selectors: Vec<FixedBytes<4>> = match get_artifact_source(state, path)? {
396            ArtifactSource::InMemory(data) => data.abi.functions().map(|f| f.selector()).collect(),
397            ArtifactSource::Disk(path) => {
398                let data = read_artifact_file(state, &path)?;
399                // Parse as raw JSON rather than `ContractObject` so we can still read selectors
400                // from artifacts with unlinked bytecode (which `ContractObject` rejects).
401                let json: serde_json::Value = serde_json::from_str(&data)?;
402                let abi =
403                    json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
404                let abi: alloy_json_abi::JsonAbi =
405                    serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))?;
406                abi.functions().map(|f| f.selector()).collect()
407            }
408        };
409        Ok(selectors.abi_encode())
410    }
411}
412
413impl Cheatcode for deployCode_0Call {
414    fn apply_full<FEN: FoundryEvmNetwork>(
415        &self,
416        ccx: &mut CheatsCtxt<'_, '_, FEN>,
417        executor: &mut dyn CheatcodesExecutor<FEN>,
418    ) -> Result {
419        let Self { artifactPath: path } = self;
420        deploy_code(ccx, executor, path, None, None, None)
421    }
422}
423
424impl Cheatcode for deployCode_1Call {
425    fn apply_full<FEN: FoundryEvmNetwork>(
426        &self,
427        ccx: &mut CheatsCtxt<'_, '_, FEN>,
428        executor: &mut dyn CheatcodesExecutor<FEN>,
429    ) -> Result {
430        let Self { artifactPath: path, constructorArgs: args } = self;
431        deploy_code(ccx, executor, path, Some(args), None, None)
432    }
433}
434
435impl Cheatcode for deployCode_2Call {
436    fn apply_full<FEN: FoundryEvmNetwork>(
437        &self,
438        ccx: &mut CheatsCtxt<'_, '_, FEN>,
439        executor: &mut dyn CheatcodesExecutor<FEN>,
440    ) -> Result {
441        let Self { artifactPath: path, value } = self;
442        deploy_code(ccx, executor, path, None, Some(*value), None)
443    }
444}
445
446impl Cheatcode for deployCode_3Call {
447    fn apply_full<FEN: FoundryEvmNetwork>(
448        &self,
449        ccx: &mut CheatsCtxt<'_, '_, FEN>,
450        executor: &mut dyn CheatcodesExecutor<FEN>,
451    ) -> Result {
452        let Self { artifactPath: path, constructorArgs: args, value } = self;
453        deploy_code(ccx, executor, path, Some(args), Some(*value), None)
454    }
455}
456
457impl Cheatcode for deployCode_4Call {
458    fn apply_full<FEN: FoundryEvmNetwork>(
459        &self,
460        ccx: &mut CheatsCtxt<'_, '_, FEN>,
461        executor: &mut dyn CheatcodesExecutor<FEN>,
462    ) -> Result {
463        let Self { artifactPath: path, salt } = self;
464        deploy_code(ccx, executor, path, None, None, Some((*salt).into()))
465    }
466}
467
468impl Cheatcode for deployCode_5Call {
469    fn apply_full<FEN: FoundryEvmNetwork>(
470        &self,
471        ccx: &mut CheatsCtxt<'_, '_, FEN>,
472        executor: &mut dyn CheatcodesExecutor<FEN>,
473    ) -> Result {
474        let Self { artifactPath: path, constructorArgs: args, salt } = self;
475        deploy_code(ccx, executor, path, Some(args), None, Some((*salt).into()))
476    }
477}
478
479impl Cheatcode for deployCode_6Call {
480    fn apply_full<FEN: FoundryEvmNetwork>(
481        &self,
482        ccx: &mut CheatsCtxt<'_, '_, FEN>,
483        executor: &mut dyn CheatcodesExecutor<FEN>,
484    ) -> Result {
485        let Self { artifactPath: path, value, salt } = self;
486        deploy_code(ccx, executor, path, None, Some(*value), Some((*salt).into()))
487    }
488}
489
490impl Cheatcode for deployCode_7Call {
491    fn apply_full<FEN: FoundryEvmNetwork>(
492        &self,
493        ccx: &mut CheatsCtxt<'_, '_, FEN>,
494        executor: &mut dyn CheatcodesExecutor<FEN>,
495    ) -> Result {
496        let Self { artifactPath: path, constructorArgs: args, value, salt } = self;
497        deploy_code(ccx, executor, path, Some(args), Some(*value), Some((*salt).into()))
498    }
499}
500
501/// Helper function to deploy contract from artifact code.
502/// Uses CREATE2 scheme if salt specified.
503fn deploy_code<FEN: FoundryEvmNetwork>(
504    ccx: &mut CheatsCtxt<'_, '_, FEN>,
505    executor: &mut dyn CheatcodesExecutor<FEN>,
506    path: &str,
507    constructor_args: Option<&Bytes>,
508    value: Option<U256>,
509    salt: Option<U256>,
510) -> Result {
511    // Synthetic creation bypasses the CREATE opcode's static-context check.
512    if ccx.is_static {
513        return Err(crate::Error::from(Bytes::new()));
514    }
515
516    let mut bytecode = get_artifact_code(ccx.state, path, false)?.to_vec();
517
518    let depth = ccx.depth();
519
520    // Broadcast the synthetic create only if it was requested by the broadcaster at the broadcast
521    // depth, as for native creates.
522    if let Some(broadcast) = &mut ccx.state.broadcast
523        && depth == broadcast.depth
524        && ccx.caller == broadcast.original_caller
525    {
526        broadcast.deploy_from_code = true;
527    }
528
529    if let Some(args) = constructor_args {
530        bytecode.extend_from_slice(args);
531    }
532
533    let scheme =
534        if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create };
535
536    // The nested EVM executes the synthetic create one level deeper, so apply the prank at the
537    // original depth just as the native create inspector would.
538    let mut caller = ccx.caller;
539    if let Some(prank) = ccx.state.get_prank(depth)
540        && let Some(changes) = prank.changes_for(depth, caller)
541    {
542        if let Some(new_caller) = changes.caller {
543            caller = new_caller;
544        }
545        if let Some(new_origin) = changes.origin {
546            ccx.ecx.tx_mut().set_caller(new_origin);
547        }
548        if let Some(used) = changes.used {
549            ccx.state.pranks.insert(depth, used);
550        }
551    }
552
553    ccx.state.deploy_code_depth = Some(depth);
554    let outcome = exec_create(
555        executor,
556        CreateInputs::new(
557            caller,
558            scheme,
559            value.unwrap_or(U256::ZERO),
560            bytecode.into(),
561            ccx.gas_limit,
562            0,
563        ),
564        ccx,
565    );
566    ccx.state.deploy_code_depth = None;
567
568    // Clear the flag in case the synthetic create was not broadcast, and end a single-call
569    // broadcast at the original depth as native create cleanup would.
570    if let Some(broadcast) = &mut ccx.state.broadcast {
571        broadcast.deploy_from_code = false;
572        if broadcast.single_call && depth == broadcast.depth {
573            ccx.ecx.tx_mut().set_caller(broadcast.original_origin);
574            ccx.state.broadcast = None;
575        }
576    }
577
578    // Restore the prank state at the original depth as native create cleanup would.
579    if let Some(prank) = ccx.state.get_prank(depth).copied()
580        && depth == prank.depth
581    {
582        ccx.ecx.tx_mut().set_caller(prank.prank_origin);
583        if prank.single_call {
584            ccx.state.pranks.remove(&depth);
585        }
586    }
587
588    let outcome = outcome?;
589
590    if !outcome.result.result.is_ok() {
591        return Err(crate::Error::from(outcome.result.output));
592    }
593
594    let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?;
595
596    Ok(address.abi_encode())
597}
598
599/// Resolved location of an artifact referenced by a cheatcode path argument.
600enum ArtifactSource<'a> {
601    /// The artifact was matched in the in-memory `available_artifacts` list.
602    InMemory(&'a ContractData),
603    /// The artifact must be read from the given path on disk.
604    Disk(PathBuf),
605}
606
607/// Resolves a cheatcode artifact reference to its source.
608///
609/// Can parse the following input formats:
610/// - `path/to/artifact.json`
611/// - `path/to/contract.sol`
612/// - `path/to/contract.sol:ContractName`
613/// - `path/to/contract.sol:ContractName:0.8.23`
614/// - `path/to/contract.sol:ContractName:profile`
615/// - `path/to/contract.sol:0.8.23`
616/// - `path/to/contract.sol:profile`
617/// - `ContractName`
618/// - `ContractName:0.8.23`
619/// - `ContractName:profile`
620fn get_artifact_source<'a, FEN: FoundryEvmNetwork>(
621    state: &'a Cheatcodes<FEN>,
622    path: &str,
623) -> Result<ArtifactSource<'a>> {
624    if path.ends_with(".json") {
625        let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
626        return Ok(ArtifactSource::Disk(path));
627    }
628
629    let artifacts =
630        state.config.available_artifacts.as_ref().or(state.config.artifact_lookup.as_ref());
631    let resolve_source = |file: PathBuf| {
632        let cwd = state
633            .config
634            .running_artifact
635            .as_ref()
636            .and_then(|artifact| artifact.source.parent())
637            .unwrap_or(&state.config.paths.root);
638        let relative_cwd = cwd.strip_prefix(&state.config.paths.root).unwrap_or(cwd);
639        let has_matching_remapping = state.config.paths.remappings.iter().any(|remapping| {
640            remapping.context.as_ref().is_none_or(|context| relative_cwd.starts_with(context))
641                && file.strip_prefix(&remapping.name).is_ok()
642        });
643
644        if has_matching_remapping {
645            state.config.paths.resolve_library_import(cwd, &file).map_or(file, |resolved| {
646                resolved.strip_prefix(&state.config.paths.root).unwrap_or(&resolved).to_path_buf()
647            })
648        } else {
649            file
650        }
651    };
652    let exact_identifier = path
653        .rsplit_once(':')
654        .map(|(source, contract)| (resolve_source(PathBuf::from(source)), contract))
655        .filter(|(source, contract)| {
656            artifacts.into_iter().flat_map(|artifacts| artifacts.iter()).any(|(id, _)| {
657                id.source == *source
658                    && id.name.split('.').next().is_some_and(|name| name == *contract)
659            })
660        });
661
662    let parsed = match parse_artifact_path(path) {
663        Ok(parsed) => parsed,
664        Err(_) if exact_identifier.is_some() => {
665            ParsedArtifactPath { file: None, contract_name: None, version: None, profile: None }
666        }
667        Err(error) => return Err(fmt_err!("failed to parse artifact path: {error}")),
668    };
669    let ParsedArtifactPath { file, contract_name, version, profile } = parsed;
670    let file = file.map(resolve_source);
671
672    // Use the artifact lookup if present.
673    if let Some(artifacts) = artifacts {
674        let ambiguous_file_profile =
675            file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some();
676        let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> {
677            artifacts
678                .iter()
679                .filter(|(id, _)| {
680                    if let Some((source, contract)) = &exact_identifier {
681                        return id.source == *source
682                            && id.name.split('.').next().is_some_and(|name| name == *contract);
683                    }
684
685                    // name might be in the form of "Counter.0.8.23"
686                    let id_name = id.name.split('.').next().unwrap();
687
688                    if let Some(path) = &file
689                        && !id.source.ends_with(path)
690                    {
691                        return false;
692                    }
693                    if let Some(ref version) = version
694                        && (id.version.minor != version.minor
695                            || id.version.major != version.major
696                            || id.version.patch != version.patch)
697                    {
698                        return false;
699                    }
700                    if let Some(profile) = profile
701                        && id.profile != profile
702                    {
703                        return false;
704                    }
705                    if let Some(name) = contract_name {
706                        if treat_ambiguous_as_profile && ambiguous_file_profile {
707                            return id.profile == name;
708                        }
709
710                        return id_name == name;
711                    }
712
713                    true
714                })
715                .collect()
716        };
717
718        let mut filtered = filter_artifacts(false);
719        if filtered.is_empty() && ambiguous_file_profile {
720            filtered = filter_artifacts(true);
721        }
722
723        let artifact = match &filtered[..] {
724            [] => None,
725            [artifact] => Some(Ok(*artifact)),
726            filtered => {
727                let mut filtered = filtered.to_vec();
728                // If we know the current script/test contract solc version, try to filter by it
729                Some(
730                    state
731                        .config
732                        .running_artifact
733                        .as_ref()
734                        .and_then(|running| {
735                            // Only filter by running version if user did NOT specify a version
736                            if exact_identifier.is_some() || version.is_none() {
737                                filtered.retain(|(id, _)| id.version == running.version);
738
739                                // Return artifact if only one matched
740                                if filtered.len() == 1 {
741                                    return Some(filtered[0]);
742                                }
743                            }
744
745                            // Only filter by running profile if user did NOT specify a profile
746                            if exact_identifier.is_some() || profile.is_none() {
747                                filtered.retain(|(id, _)| id.profile == running.profile);
748
749                                return (filtered.len() == 1).then(|| filtered[0]);
750                            }
751
752                            None
753                        })
754                        .ok_or_else(|| fmt_err!("multiple matching artifacts found")),
755                )
756            }
757        };
758
759        if let Some(artifact) = artifact {
760            return Ok(ArtifactSource::InMemory(artifact?.1));
761        }
762    }
763
764    // Fallback: construct path manually when no artifacts list or no match found
765    let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
766        (Some(file), Some(contract_name)) => PathBuf::from(format!("{file}/{contract_name}.json")),
767        (None, Some(contract_name)) => {
768            PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
769        }
770        (Some(file), None) => {
771            let name = file.replace(".sol", "");
772            PathBuf::from(format!("{file}/{name}.json"))
773        }
774        _ => bail!("invalid artifact path"),
775    };
776
777    let path = state.config.paths.artifacts.join(path_in_artifacts);
778    let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
779    Ok(ArtifactSource::Disk(path))
780}
781
782/// Reads an artifact JSON file, mapping I/O errors to a helpful message when the
783/// lookup fell through the in-memory artifacts list.
784fn read_artifact_file<FEN: FoundryEvmNetwork>(
785    state: &Cheatcodes<FEN>,
786    path: &Path,
787) -> Result<String> {
788    fs::read_to_string(path).map_err(|e| {
789        if state.config.available_artifacts.is_some() {
790            fmt_err!("no matching artifact found")
791        } else {
792            e.into()
793        }
794    })
795}
796
797/// Returns the bytecode from a JSON artifact file.
798///
799/// See [`get_artifact_source`] for the supported path formats.
800///
801/// This function is safe to use with contracts that have library dependencies.
802/// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will
803/// reject artifacts with unlinked library placeholders.
804fn get_artifact_code<FEN: FoundryEvmNetwork>(
805    state: &Cheatcodes<FEN>,
806    path: &str,
807    deployed: bool,
808) -> Result<Bytes> {
809    let maybe_bytecode = match get_artifact_source(state, path)? {
810        ArtifactSource::InMemory(data) => {
811            if deployed { data.deployed_bytecode() } else { data.bytecode() }.cloned()
812        }
813        ArtifactSource::Disk(path) => {
814            let data = read_artifact_file(state, &path)?;
815            let artifact = serde_json::from_str::<ContractObject>(&data)?;
816            if deployed { artifact.deployed_bytecode } else { artifact.bytecode }
817        }
818    };
819    maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
820}
821
822impl Cheatcode for ffiCall {
823    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
824        let Self { commandInput: input } = self;
825        let stdout = ffi_stdout(state, input)?;
826        Ok(decode_ffi_stdout(&stdout).abi_encode())
827    }
828}
829
830impl Cheatcode for ffiUintCall {
831    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
832        let Self { commandInput: input } = self;
833        parse(&ffi_stdout(state, input)?, &DynSolType::Uint(256))
834    }
835}
836
837impl Cheatcode for ffiStringCall {
838    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
839        let Self { commandInput: input } = self;
840        Ok(ffi_stdout(state, input)?.abi_encode())
841    }
842}
843
844impl Cheatcode for ffiBytesCall {
845    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
846        let Self { commandInput: input } = self;
847        let stdout = ffi_stdout(state, input)?;
848        Ok(hex::decode(&stdout)
849            .map_err(|err| fmt_err!("failed parsing ffi stdout as bytes: {err}"))?
850            .abi_encode())
851    }
852}
853
854impl Cheatcode for tryFfiCall {
855    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
856        let Self { commandInput: input } = self;
857        ffi(state, input).map(|res| res.abi_encode())
858    }
859}
860
861impl Cheatcode for promptCall {
862    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
863        let Self { promptText: text } = self;
864        prompt(state, text, prompt_input).map(|res| res.abi_encode())
865    }
866}
867
868impl Cheatcode for promptSecretCall {
869    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
870        let Self { promptText: text } = self;
871        prompt(state, text, prompt_password).map(|res| res.abi_encode())
872    }
873}
874
875impl Cheatcode for promptSecretUintCall {
876    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
877        let Self { promptText: text } = self;
878        parse(&prompt(state, text, prompt_password)?, &DynSolType::Uint(256))
879    }
880}
881
882impl Cheatcode for promptAddressCall {
883    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
884        let Self { promptText: text } = self;
885        parse(&prompt(state, text, prompt_input)?, &DynSolType::Address)
886    }
887}
888
889impl Cheatcode for promptUintCall {
890    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
891        let Self { promptText: text } = self;
892        parse(&prompt(state, text, prompt_input)?, &DynSolType::Uint(256))
893    }
894}
895
896pub(super) fn write_file<FEN: FoundryEvmNetwork>(
897    state: &Cheatcodes<FEN>,
898    path: &Path,
899    contents: &[u8],
900) -> Result {
901    let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
902    // write access to foundry.toml is not allowed
903    state.config.ensure_not_foundry_toml(&path)?;
904
905    if state.fs_commit {
906        fs::locked_write(path, contents)?;
907    }
908
909    Ok(Default::default())
910}
911
912fn read_dir<FEN: FoundryEvmNetwork>(
913    state: &Cheatcodes<FEN>,
914    path: &Path,
915    max_depth: u64,
916    follow_links: bool,
917) -> Result {
918    let root = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
919    let paths: Vec<DirEntry> = WalkDir::new(root)
920        .min_depth(1)
921        .max_depth(max_depth.try_into().unwrap_or(usize::MAX))
922        .follow_links(follow_links)
923        .contents_first(false)
924        .same_file_system(true)
925        .sort_by_file_name()
926        .into_iter()
927        .map(|entry| match entry {
928            Ok(entry) => DirEntry {
929                errorMessage: String::new(),
930                path: entry.path().display().to_string(),
931                depth: entry.depth() as u64,
932                isDir: entry.file_type().is_dir(),
933                isSymlink: entry.path_is_symlink(),
934            },
935            Err(e) => DirEntry {
936                errorMessage: e.to_string(),
937                path: e.path().map(|p| p.display().to_string()).unwrap_or_default(),
938                depth: e.depth() as u64,
939                isDir: false,
940                isSymlink: false,
941            },
942        })
943        .collect();
944    Ok(paths.abi_encode())
945}
946
947fn ffi<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<FfiResult> {
948    let output = ffi_command(state, input)?;
949
950    let stdout = String::from_utf8(output.stdout)?;
951    let stdout = stdout.trim();
952    Ok(FfiResult {
953        exitCode: output.status.code().unwrap_or(69),
954        stdout: decode_ffi_stdout(stdout),
955        stderr: output.stderr.into(),
956    })
957}
958
959fn ffi_stdout<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>, input: &[String]) -> Result<String> {
960    let output = ffi_command(state, input)?;
961    let stdout = String::from_utf8(output.stdout)?;
962    if !output.status.success() {
963        return Err(fmt_err!(
964            "ffi command {:?} exited with code {}. stderr: {}",
965            input,
966            output.status.code().unwrap_or(69),
967            String::from_utf8_lossy(&output.stderr)
968        ));
969    }
970    if !output.stderr.is_empty() {
971        let stderr = String::from_utf8_lossy(&output.stderr);
972        warn!(target: "cheatcodes", ?input, ?stderr, "ffi command wrote to stderr");
973    }
974    Ok(stdout.trim().to_string())
975}
976
977fn decode_ffi_stdout(stdout: &str) -> Bytes {
978    hex::decode(stdout).unwrap_or_else(|_| stdout.as_bytes().to_vec()).into()
979}
980
981fn ffi_command<FEN: FoundryEvmNetwork>(
982    state: &Cheatcodes<FEN>,
983    input: &[String],
984) -> Result<std::process::Output> {
985    ensure!(
986        state.config.ffi,
987        "FFI is disabled; add the `--ffi` flag to allow tests to call external commands"
988    );
989    ensure!(!input.is_empty() && !input[0].is_empty(), "can't execute empty command");
990    let mut cmd = Command::new(&input[0]);
991    cmd.args(&input[1..]);
992
993    debug!(target: "cheatcodes", ?cmd, "invoking ffi");
994
995    cmd.current_dir(&state.config.root)
996        .output()
997        .map_err(|err| fmt_err!("failed to execute command {cmd:?}: {err}"))
998}
999
1000fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> {
1001    Input::new().allow_empty(true).with_prompt(prompt_text).interact_text()
1002}
1003
1004fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> {
1005    Password::new().with_prompt(prompt_text).interact()
1006}
1007
1008fn prompt<FEN: FoundryEvmNetwork>(
1009    state: &Cheatcodes<FEN>,
1010    prompt_text: &str,
1011    input: fn(&str) -> Result<String, dialoguer::Error>,
1012) -> Result<String> {
1013    let text_clone = prompt_text.to_string();
1014    let timeout = state.config.prompt_timeout;
1015    let (tx, rx) = mpsc::channel();
1016
1017    thread::spawn(move || {
1018        let _ = tx.send(input(&text_clone));
1019    });
1020
1021    match rx.recv_timeout(timeout) {
1022        Ok(res) => res.map_err(|err| {
1023            let _ = sh_println!();
1024            err.to_string().into()
1025        }),
1026        Err(_) => {
1027            let _ = sh_eprintln!();
1028            Err("Prompt timed out".into())
1029        }
1030    }
1031}
1032
1033impl Cheatcode for getBroadcastCall {
1034    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1035        let Self { contractName, chainId, txType } = self;
1036
1037        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1038            contractName,
1039            *chainId,
1040            &state.config.broadcast,
1041            vec![map_broadcast_tx_type(*txType)],
1042        )?;
1043
1044        Ok(latest_broadcast.abi_encode())
1045    }
1046}
1047
1048impl Cheatcode for getBroadcasts_0Call {
1049    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1050        let Self { contractName, chainId, txType } = self;
1051
1052        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1053            .with_tx_type(map_broadcast_tx_type(*txType));
1054
1055        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1056
1057        let summaries = broadcasts
1058            .into_iter()
1059            .flat_map(|broadcast| {
1060                let results = reader.into_tx_receipts(broadcast);
1061                parse_broadcast_results(results)
1062            })
1063            .collect::<Vec<_>>();
1064
1065        Ok(summaries.abi_encode())
1066    }
1067}
1068
1069impl Cheatcode for getBroadcasts_1Call {
1070    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1071        let Self { contractName, chainId } = self;
1072
1073        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?;
1074
1075        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1076
1077        let summaries = broadcasts
1078            .into_iter()
1079            .flat_map(|broadcast| {
1080                let results = reader.into_tx_receipts(broadcast);
1081                parse_broadcast_results(results)
1082            })
1083            .collect::<Vec<_>>();
1084
1085        Ok(summaries.abi_encode())
1086    }
1087}
1088
1089impl Cheatcode for getDeployment_0Call {
1090    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
1091        let Self { contractName } = self;
1092        let chain_id = ccx.chain_id();
1093
1094        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1095            contractName,
1096            chain_id,
1097            &ccx.state.config.broadcast,
1098            vec![CallKind::Create, CallKind::Create2],
1099        )?;
1100
1101        Ok(latest_broadcast.contractAddress.abi_encode())
1102    }
1103}
1104
1105impl Cheatcode for getDeployment_1Call {
1106    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1107        let Self { contractName, chainId } = self;
1108
1109        let latest_broadcast = latest_broadcast::<<FEN as FoundryEvmNetwork>::Network>(
1110            contractName,
1111            *chainId,
1112            &state.config.broadcast,
1113            vec![CallKind::Create, CallKind::Create2],
1114        )?;
1115
1116        Ok(latest_broadcast.contractAddress.abi_encode())
1117    }
1118}
1119
1120impl Cheatcode for getDeploymentsCall {
1121    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
1122        let Self { contractName, chainId } = self;
1123
1124        let reader = BroadcastReader::new(contractName.clone(), *chainId, &state.config.broadcast)?
1125            .with_tx_type(CallKind::Create)
1126            .with_tx_type(CallKind::Create2);
1127
1128        let broadcasts = reader.read::<<FEN as FoundryEvmNetwork>::Network>()?;
1129
1130        let summaries = broadcasts
1131            .into_iter()
1132            .flat_map(|broadcast| {
1133                let results = reader.into_tx_receipts(broadcast);
1134                parse_broadcast_results(results)
1135            })
1136            .collect::<Vec<_>>();
1137
1138        let deployed_addresses =
1139            summaries.into_iter().map(|summary| summary.contractAddress).collect::<Vec<_>>();
1140
1141        Ok(deployed_addresses.abi_encode())
1142    }
1143}
1144
1145fn map_broadcast_tx_type(tx_type: BroadcastTxType) -> CallKind {
1146    match tx_type {
1147        BroadcastTxType::Call => CallKind::Call,
1148        BroadcastTxType::Create => CallKind::Create,
1149        BroadcastTxType::Create2 => CallKind::Create2,
1150        _ => unreachable!("invalid tx type"),
1151    }
1152}
1153
1154fn parse_broadcast_results<N: Network>(
1155    results: Vec<(TransactionWithMetadata<N>, N::ReceiptResponse)>,
1156) -> Vec<BroadcastTxSummary> {
1157    results
1158        .into_iter()
1159        .map(|(tx, receipt)| BroadcastTxSummary {
1160            txHash: receipt.transaction_hash(),
1161            blockNumber: receipt.block_number().unwrap_or_default(),
1162            txType: match tx.call_kind {
1163                CallKind::Call => BroadcastTxType::Call,
1164                CallKind::Create => BroadcastTxType::Create,
1165                CallKind::Create2 => BroadcastTxType::Create2,
1166                _ => unreachable!("invalid tx type"),
1167            },
1168            contractAddress: tx.contract_address.unwrap_or_default(),
1169            success: receipt.status(),
1170        })
1171        .collect()
1172}
1173
1174fn latest_broadcast<N: Network>(
1175    contract_name: &String,
1176    chain_id: u64,
1177    broadcast_path: &Path,
1178    filters: Vec<CallKind>,
1179) -> Result<BroadcastTxSummary>
1180where
1181    N::TxEnvelope: for<'d> serde::Deserialize<'d>,
1182{
1183    let mut reader = BroadcastReader::new(contract_name.clone(), chain_id, broadcast_path)?;
1184
1185    for filter in filters {
1186        reader = reader.with_tx_type(filter);
1187    }
1188
1189    let broadcast = reader.read_latest::<N>()?;
1190
1191    let results = reader.into_tx_receipts(broadcast);
1192
1193    let summaries = parse_broadcast_results(results);
1194
1195    summaries
1196        .first()
1197        .ok_or_else(|| fmt_err!("no deployment found for {contract_name} on chain {chain_id}"))
1198        .cloned()
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204    use crate::CheatsConfig;
1205    use alloy_primitives::{Bloom, address, b256};
1206    use foundry_common::ContractsByArtifact;
1207    use foundry_compilers::{
1208        ArtifactId,
1209        artifacts::{
1210            BytecodeObject, CompactBytecode, CompactContractBytecode, remappings::Remapping,
1211        },
1212    };
1213    use std::{env, fs as stdfs, str::FromStr, sync::Arc};
1214    use tempfile::TempDir;
1215    use tempo_alloy::TempoNetwork;
1216
1217    fn cheats() -> Cheatcodes {
1218        let config = CheatsConfig {
1219            ffi: true,
1220            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1221            ..Default::default()
1222        };
1223        Cheatcodes::new(Arc::new(config))
1224    }
1225
1226    #[test]
1227    fn test_ffi_hex() {
1228        let msg = b"gm";
1229        let cheats = cheats();
1230        let args = ["echo".to_string(), hex::encode(msg)];
1231        let output = ffi(&cheats, &args).unwrap();
1232        assert_eq!(output.stdout, Bytes::from(msg));
1233    }
1234
1235    #[test]
1236    fn test_ffi_string() {
1237        let msg = "gm";
1238        let cheats = cheats();
1239        let args = ["echo".to_string(), msg.to_string()];
1240        let output = ffi(&cheats, &args).unwrap();
1241        assert_eq!(output.stdout, Bytes::from(msg.as_bytes()));
1242    }
1243
1244    #[test]
1245    fn test_ffi_fails_on_error_code() {
1246        let mut cheats = cheats();
1247
1248        // Use a command that is guaranteed to fail with a non-zero exit code on any platform.
1249        #[cfg(unix)]
1250        let args = vec!["false".to_string()];
1251        #[cfg(windows)]
1252        let args = vec!["cmd".to_string(), "/c".to_string(), "exit 1".to_string()];
1253
1254        let result = Cheatcode::apply(&ffiCall { commandInput: args }, &mut cheats);
1255
1256        // Assert that the cheatcode returned an error.
1257        assert!(result.is_err(), "Expected ffi cheatcode to fail, but it succeeded");
1258
1259        // Assert that the error message contains the expected information.
1260        let err_msg = result.unwrap_err().to_string();
1261        assert!(
1262            err_msg.contains("exited with code 1"),
1263            "Error message did not contain exit code: {err_msg}"
1264        );
1265    }
1266
1267    #[test]
1268    fn test_artifact_parsing() {
1269        let s = include_str!("../../evm/test-data/solc-obj.json");
1270        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1271        assert!(artifact.bytecode.is_some());
1272
1273        let artifact: ContractObject = serde_json::from_str(s).unwrap();
1274        assert!(artifact.deployed_bytecode.is_some());
1275    }
1276
1277    #[test]
1278    fn test_alloy_json_abi_rejects_unlinked_bytecode() {
1279        let artifact_json = r#"{
1280            "abi": [],
1281            "bytecode": "0x73__$987e73aeca5e61ce83e4cb0814d87beda9$__63baf2f868"
1282        }"#;
1283
1284        let result: Result<ContractObject, _> = serde_json::from_str(artifact_json);
1285        assert!(result.is_err(), "should reject unlinked bytecode with placeholders");
1286        let err = result.unwrap_err().to_string();
1287        assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder"));
1288    }
1289
1290    #[test]
1291    fn test_parse_artifact_path_file_only() {
1292        let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap();
1293        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1294        assert_eq!(parsed.contract_name, None);
1295        assert_eq!(parsed.version, None);
1296        assert_eq!(parsed.profile, None);
1297    }
1298
1299    #[test]
1300    fn test_parse_artifact_path_windows_drive() {
1301        for file in [
1302            "C:/project/src/Contract.sol",
1303            r"C:\project\src\Contract.sol",
1304            r"\\?\C:\project\src\Contract.sol",
1305            r"\\server\share\Contract.sol",
1306        ] {
1307            for (suffix, contract_name, version, profile) in [
1308                ("", None, None, None),
1309                (":MyContract", Some("MyContract"), None, None),
1310                (":0.8.23", None, Some(Version::new(0, 8, 23)), None),
1311                (":MyContract:0.8.23", Some("MyContract"), Some(Version::new(0, 8, 23)), None),
1312                (":MyContract:optimized", Some("MyContract"), None, Some("optimized")),
1313            ] {
1314                let input = format!("{file}{suffix}");
1315                assert_eq!(
1316                    parse_artifact_path(&input).unwrap(),
1317                    ParsedArtifactPath {
1318                        file: Some(PathBuf::from(file)),
1319                        contract_name,
1320                        version,
1321                        profile,
1322                    },
1323                    "{input}"
1324                );
1325            }
1326        }
1327
1328        // A one-letter contract name must still allow version and profile suffixes.
1329        assert_eq!(parse_artifact_path("C:0.8.23").unwrap().version, Some(Version::new(0, 8, 23)));
1330        assert_eq!(parse_artifact_path("C:optimized").unwrap().profile, Some("optimized"));
1331    }
1332
1333    #[test]
1334    fn test_parse_artifact_path_file_and_contract() {
1335        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap();
1336        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1337        assert_eq!(parsed.contract_name, Some("MyContract"));
1338        assert_eq!(parsed.version, None);
1339        assert_eq!(parsed.profile, None);
1340    }
1341
1342    #[test]
1343    fn test_parse_artifact_path_file_contract_version() {
1344        let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap();
1345        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1346        assert_eq!(parsed.contract_name, Some("MyContract"));
1347        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1348        assert_eq!(parsed.profile, None);
1349    }
1350
1351    #[test]
1352    fn test_parse_artifact_path_file_contract_profile() {
1353        let parsed =
1354            super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap();
1355        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1356        assert_eq!(parsed.contract_name, Some("MyContract"));
1357        assert_eq!(parsed.version, None);
1358        assert_eq!(parsed.profile, Some("optimized"));
1359    }
1360
1361    #[test]
1362    fn test_parse_artifact_path_file_and_version() {
1363        let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap();
1364        assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol")));
1365        assert_eq!(parsed.contract_name, None);
1366        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18)));
1367        assert_eq!(parsed.profile, None);
1368    }
1369
1370    #[test]
1371    fn test_parse_artifact_path_file_and_profile() {
1372        // The parser keeps the two-part file form ambiguous. Artifact lookup can resolve this
1373        // segment as a profile when no contract name matches.
1374        let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap();
1375        assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol")));
1376        assert_eq!(parsed.contract_name, Some("paris"));
1377        assert_eq!(parsed.version, None);
1378        assert_eq!(parsed.profile, None);
1379    }
1380
1381    fn test_artifact(
1382        source: &str,
1383        name: &str,
1384        profile: &str,
1385        bytecode: Bytes,
1386    ) -> (ArtifactId, CompactContractBytecode) {
1387        (
1388            ArtifactId {
1389                path: PathBuf::from(format!("{source}/{name}.json")),
1390                name: name.to_owned(),
1391                source: PathBuf::from(source),
1392                version: Version::new(0, 8, 30),
1393                build_id: String::new(),
1394                profile: profile.to_owned(),
1395            },
1396            CompactContractBytecode {
1397                abi: Some(Default::default()),
1398                bytecode: Some(CompactBytecode {
1399                    object: BytecodeObject::Bytecode(bytecode),
1400                    source_map: None,
1401                    link_references: Default::default(),
1402                }),
1403                deployed_bytecode: None,
1404            },
1405        )
1406    }
1407
1408    #[test]
1409    fn test_get_artifact_code_resolves_file_profile_ambiguity() {
1410        let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1411        let paris_bytecode = Bytes::from_static(&[0x60, 0x02]);
1412        let source = "src/GetCodeProfile.t.sol";
1413        let artifacts = ContractsByArtifact::new([
1414            test_artifact(source, "GetCodeProfile", "default", default_bytecode),
1415            test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()),
1416        ]);
1417        let config = CheatsConfig {
1418            available_artifacts: Some(artifacts),
1419            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1420            ..Default::default()
1421        };
1422        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1423
1424        let bytecode =
1425            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1426
1427        assert_eq!(bytecode, paris_bytecode);
1428    }
1429
1430    #[test]
1431    fn test_get_artifact_code_exact_colon_path_uses_running_profile() {
1432        let default_bytecode = Bytes::from_static(&[0x60, 0x01]);
1433        let optimized_bytecode = Bytes::from_static(&[0x60, 0x02]);
1434        let versioned_bytecode = Bytes::from_static(&[0x60, 0x03]);
1435        let source = "src/Colon:Path.sol";
1436        let default = test_artifact(source, "Target", "default", default_bytecode);
1437        let optimized =
1438            test_artifact(source, "Target.optimized", "optimized", optimized_bytecode.clone());
1439        let mut versioned = test_artifact(
1440            source,
1441            "Target.0.8.29.optimized",
1442            "optimized",
1443            versioned_bytecode.clone(),
1444        );
1445        versioned.0.version = Version::new(0, 8, 29);
1446        let artifacts = ContractsByArtifact::new([default, optimized, versioned]);
1447        for (patch, expected) in [(30, optimized_bytecode), (29, versioned_bytecode)] {
1448            let running_artifact = ArtifactId {
1449                source: PathBuf::from("test/Runner.t.sol"),
1450                name: "Runner".to_owned(),
1451                path: PathBuf::from("test/Runner.t.sol/Runner.json"),
1452                version: Version::new(0, 8, patch),
1453                build_id: String::new(),
1454                profile: "optimized".to_owned(),
1455            };
1456            let config = CheatsConfig {
1457                available_artifacts: Some(artifacts.clone()),
1458                running_artifact: Some(running_artifact),
1459                root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1460                ..Default::default()
1461            };
1462            let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1463            let bytecode =
1464                super::get_artifact_code(&cheats, "src/Colon:Path.sol:Target", false).unwrap();
1465            assert_eq!(bytecode, expected);
1466        }
1467    }
1468
1469    #[test]
1470    fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() {
1471        let profile_bytecode = Bytes::from_static(&[0x60, 0x02]);
1472        let contract_bytecode = Bytes::from_static(&[0x60, 0x03]);
1473        let source = "src/GetCodeProfile.t.sol";
1474        let artifacts = ContractsByArtifact::new([
1475            test_artifact(source, "GetCodeProfile", "paris", profile_bytecode),
1476            test_artifact(source, "paris", "default", contract_bytecode.clone()),
1477        ]);
1478        let config = CheatsConfig {
1479            available_artifacts: Some(artifacts),
1480            root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
1481            ..Default::default()
1482        };
1483        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1484
1485        let bytecode =
1486            super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap();
1487
1488        assert_eq!(bytecode, contract_bytecode);
1489    }
1490
1491    #[test]
1492    fn test_get_artifact_code_resolves_remapping() {
1493        let bytecode = Bytes::from_static(&[0x60, 0x01]);
1494        let artifacts = ContractsByArtifact::new([test_artifact(
1495            "src/Something.sol",
1496            "Something",
1497            "default",
1498            bytecode.clone(),
1499        )]);
1500        let root = PathBuf::from(&env!("CARGO_MANIFEST_DIR"));
1501        let paths = foundry_compilers::ProjectPathsConfig::builder()
1502            .remapping(Remapping::from_str("@example/=src/").unwrap())
1503            .build_with_root(&root);
1504        let config = CheatsConfig {
1505            available_artifacts: Some(artifacts),
1506            root,
1507            paths,
1508            ..Default::default()
1509        };
1510        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1511
1512        let resolved =
1513            super::get_artifact_code(&cheats, "@example/Something.sol:Something", false).unwrap();
1514
1515        assert_eq!(resolved, bytecode);
1516    }
1517
1518    #[test]
1519    fn test_get_artifact_code_remaps_exact_paths_before_profile_selection() {
1520        for file in ["Target.sol", "Colon:Path.sol"] {
1521            let source = format!("src/{file}");
1522            let remapped = format!("lib/alternate/{file}");
1523            let default_bytecode = Bytes::from_static(&[0x60, 0x02]);
1524            let optimized_bytecode = Bytes::from_static(&[0x60, 0x03]);
1525            let artifacts = ContractsByArtifact::new([
1526                test_artifact(&source, "Target", "default", Bytes::from_static(&[0x60, 0x01])),
1527                test_artifact(&remapped, "Target", "default", default_bytecode.clone()),
1528                test_artifact(
1529                    &remapped,
1530                    "Target.optimized",
1531                    "optimized",
1532                    optimized_bytecode.clone(),
1533                ),
1534            ]);
1535            for (profile, expected) in
1536                [("default", default_bytecode), ("optimized", optimized_bytecode)]
1537            {
1538                let root = PathBuf::from(&env!("CARGO_MANIFEST_DIR"));
1539                let paths = foundry_compilers::ProjectPathsConfig::builder()
1540                    .remapping(Remapping::from_str("src/=lib/alternate/").unwrap())
1541                    .build_with_root(&root);
1542                let config = CheatsConfig {
1543                    available_artifacts: Some(artifacts.clone()),
1544                    running_artifact: Some(
1545                        test_artifact("test/Runner.t.sol", "Runner", profile, Bytes::new()).0,
1546                    ),
1547                    root,
1548                    paths,
1549                    ..Default::default()
1550                };
1551                let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1552                let resolved =
1553                    super::get_artifact_code(&cheats, &format!("{source}:Target"), false).unwrap();
1554                assert_eq!(resolved, expected, "{source} in {profile} profile");
1555            }
1556        }
1557    }
1558
1559    #[test]
1560    fn test_get_artifact_code_preserves_project_path_on_library_collision() {
1561        let root_bytecode = Bytes::from_static(&[0x60, 0x01]);
1562        let library_bytecode = Bytes::from_static(&[0x60, 0x02]);
1563        let artifacts = ContractsByArtifact::new([
1564            test_artifact("src/Thing.sol", "RootThing", "default", root_bytecode.clone()),
1565            test_artifact("lib/src/Thing.sol", "LibThing", "default", library_bytecode),
1566        ]);
1567        let temp = TempDir::new().unwrap();
1568        let library = temp.path().join("lib");
1569        stdfs::create_dir_all(library.join("src")).unwrap();
1570        stdfs::write(library.join("src/Thing.sol"), "").unwrap();
1571        let paths = foundry_compilers::ProjectPathsConfig::builder()
1572            .remappings([])
1573            .lib(library)
1574            .build_with_root(temp.path());
1575        let config = CheatsConfig {
1576            available_artifacts: Some(artifacts),
1577            root: temp.path().to_path_buf(),
1578            paths,
1579            ..Default::default()
1580        };
1581        let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config));
1582
1583        let resolved = super::get_artifact_code(&cheats, "src/Thing.sol:RootThing", false).unwrap();
1584
1585        assert_eq!(resolved, root_bytecode);
1586    }
1587
1588    #[test]
1589    fn test_parse_artifact_path_contract_only() {
1590        let parsed = super::parse_artifact_path("MyContract").unwrap();
1591        assert_eq!(parsed.file, None);
1592        assert_eq!(parsed.contract_name, Some("MyContract"));
1593        assert_eq!(parsed.version, None);
1594        assert_eq!(parsed.profile, None);
1595    }
1596
1597    #[test]
1598    fn test_parse_artifact_path_contract_and_version() {
1599        let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap();
1600        assert_eq!(parsed.file, None);
1601        assert_eq!(parsed.contract_name, Some("MyContract"));
1602        assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23)));
1603        assert_eq!(parsed.profile, None);
1604    }
1605
1606    #[test]
1607    fn test_parse_artifact_path_contract_and_profile() {
1608        let parsed = super::parse_artifact_path("MyContract:optimized").unwrap();
1609        assert_eq!(parsed.file, None);
1610        assert_eq!(parsed.contract_name, Some("MyContract"));
1611        assert_eq!(parsed.version, None);
1612        assert_eq!(parsed.profile, Some("optimized"));
1613    }
1614
1615    #[test]
1616    fn test_parse_artifact_path_profile_names() {
1617        // Test various profile name patterns
1618        for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] {
1619            let path = format!("MyContract:{profile}");
1620            let parsed = super::parse_artifact_path(&path).unwrap();
1621            assert_eq!(parsed.contract_name, Some("MyContract"));
1622            assert_eq!(parsed.profile, Some(profile));
1623            assert_eq!(parsed.version, None);
1624        }
1625    }
1626
1627    #[test]
1628    fn test_parse_artifact_path_invalid_version() {
1629        // Invalid semver should be treated as profile
1630        let parsed = super::parse_artifact_path("MyContract:invalid").unwrap();
1631        assert_eq!(parsed.contract_name, Some("MyContract"));
1632        assert_eq!(parsed.profile, Some("invalid"));
1633        assert_eq!(parsed.version, None);
1634    }
1635
1636    fn unique_temp_dir(prefix: &str) -> PathBuf {
1637        env::temp_dir().join(format!(
1638            "foundry-cheatcodes-{prefix}-{}",
1639            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
1640        ))
1641    }
1642
1643    #[test]
1644    fn test_latest_broadcast_reads_tempo_sequences() {
1645        let root = unique_temp_dir("tempo-broadcast");
1646        let broadcast_path = root.join("broadcast");
1647        let sequence_dir = broadcast_path.join("Counter.s.sol").join("31337");
1648        stdfs::create_dir_all(&sequence_dir).unwrap();
1649
1650        let tx_hash = "0x04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f";
1651        let block_hash = "0x860f788b251ece768e63b0d3906d156f652d843848b71c7fe81faacd49139d66";
1652        let from = "0xa70ab0448e66cd77995bfbba5c5b64b41a85f3fd";
1653        let contract_address = "0x20c0000000000000000000000000000000000000";
1654        let zero_bloom = format!("{:#x}", Bloom::ZERO);
1655
1656        let sequence = serde_json::json!({
1657            "transactions": [{
1658                "hash": tx_hash,
1659                "transactionType": "CREATE",
1660                "contractName": "Counter",
1661                "contractAddress": contract_address,
1662                "function": serde_json::Value::Null,
1663                "arguments": serde_json::Value::Null,
1664                "transaction": {
1665                    "type": "0x76",
1666                    "from": from,
1667                    "to": serde_json::Value::Null,
1668                    "data": "0x",
1669                    "value": "0x0",
1670                    "gas": "0x5208",
1671                    "nonce": "0x0",
1672                    "accessList": [],
1673                    "calls": [],
1674                    "nonceKey": "0x0",
1675                    "feePayerSignature": serde_json::Value::Null,
1676                    "validBefore": serde_json::Value::Null,
1677                    "validAfter": serde_json::Value::Null,
1678                    "keyAuthorization": serde_json::Value::Null,
1679                    "aaAuthorizationList": []
1680                },
1681                "additionalContracts": [],
1682                "isFixedGasLimit": false
1683            }],
1684            "receipts": [{
1685                "type": "0x76",
1686                "status": "0x1",
1687                "cumulativeGasUsed": "0x5208",
1688                "logs": [],
1689                "logsBloom": zero_bloom,
1690                "transactionHash": tx_hash,
1691                "transactionIndex": "0x0",
1692                "blockHash": block_hash,
1693                "blockNumber": "0x7",
1694                "gasUsed": "0x5208",
1695                "effectiveGasPrice": "0x1",
1696                "from": from,
1697                "to": serde_json::Value::Null,
1698                "contractAddress": contract_address,
1699                "feePayer": from
1700            }],
1701            "libraries": [],
1702            "pending": [],
1703            "returns": {},
1704            "timestamp": 1,
1705            "chain": 31337,
1706            "commit": serde_json::Value::Null
1707        });
1708
1709        fs::write_json_file(&sequence_dir.join("run-1.json"), &sequence).unwrap();
1710
1711        let latest = latest_broadcast::<TempoNetwork>(
1712            &"Counter".to_owned(),
1713            31337,
1714            &broadcast_path,
1715            vec![CallKind::Create],
1716        )
1717        .unwrap();
1718
1719        assert_eq!(
1720            latest.txHash,
1721            b256!("04548a0ea27e2cccc1479af3c2ff02da4d4d3ea46af8e8d7edaa49f6ea27073f")
1722        );
1723        assert_eq!(latest.blockNumber, 7);
1724        assert!(matches!(latest.txType, BroadcastTxType::Create));
1725        assert_eq!(latest.contractAddress, address!("20c0000000000000000000000000000000000000"));
1726        assert!(latest.success);
1727
1728        stdfs::remove_dir_all(root).unwrap();
1729    }
1730}