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