Skip to main content

cast/cmd/
storage.rs

1use crate::opts::parse_slot;
2use alloy_ens::NameOrAddress;
3use alloy_network::AnyNetwork;
4use alloy_primitives::{Address, B256, Bytes, U256};
5use alloy_provider::Provider;
6use alloy_rpc_types::BlockId;
7use clap::Parser;
8use comfy_table::{
9    Table,
10    presets::{ASCII_FULL, ASCII_MARKDOWN},
11};
12use eyre::Result;
13use foundry_cli::{
14    opts::{BuildOpts, EtherscanOpts, RpcOpts},
15    utils,
16    utils::LoadConfig,
17};
18use foundry_common::{
19    abi::find_source,
20    compile::{ProjectCompiler, add_storage_layout_output, etherscan_project},
21    has_metadata_hash, shell,
22};
23use foundry_compilers::{
24    Artifact, ArtifactId, Project, ProjectCompileOutput,
25    artifacts::{ConfigurableContractArtifact, StorageLayout},
26    compilers::solc::{Solc, SolcCompiler},
27};
28use foundry_config::{
29    Config,
30    figment::{self, Metadata, Profile, value::Dict},
31    impl_figment_convert_cast,
32};
33use semver::Version;
34use serde::{Deserialize, Serialize};
35use std::{collections::BTreeSet, str::FromStr};
36
37/// The minimum Solc version for outputting storage layouts.
38///
39/// <https://github.com/ethereum/solidity/blob/develop/Changelog.md#065-2020-04-06>
40const MIN_SOLC: Version = Version::new(0, 6, 5);
41
42/// CLI arguments for `cast storage`.
43#[derive(Clone, Debug, Parser)]
44pub struct StorageArgs {
45    /// The contract address.
46    #[arg(value_parser = NameOrAddress::from_str)]
47    address: NameOrAddress,
48
49    /// The storage slot number. If not provided, it gets the full storage layout.
50    #[arg(value_parser = parse_slot)]
51    base_slot: Option<B256>,
52
53    /// The storage offset from the base slot. If not provided, it is assumed to be zero.
54    #[arg(value_parser = str::parse::<U256>, default_value_t = U256::ZERO)]
55    offset: U256,
56
57    /// The known proxy address. If provided, the storage layout is retrieved from this address.
58    #[arg(long,value_parser = NameOrAddress::from_str)]
59    proxy: Option<NameOrAddress>,
60
61    /// The block height to query at.
62    ///
63    /// Can also be the tags earliest, finalized, safe, latest, or pending.
64    #[arg(long, short)]
65    block: Option<BlockId>,
66
67    #[command(flatten)]
68    rpc: RpcOpts,
69
70    #[command(flatten)]
71    etherscan: EtherscanOpts,
72
73    #[command(flatten)]
74    build: BuildOpts,
75
76    /// Specify the solc version to compile with. Overrides detected version.
77    #[arg(long, value_parser = Version::parse)]
78    solc_version: Option<Version>,
79}
80
81impl_figment_convert_cast!(StorageArgs);
82
83impl figment::Provider for StorageArgs {
84    fn metadata(&self) -> Metadata {
85        Metadata::named("StorageArgs")
86    }
87
88    fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
89        let mut map = self.build.data()?;
90        let dict = map.get_mut(&Config::selected_profile()).unwrap();
91        dict.extend(self.rpc.dict());
92        dict.extend(self.etherscan.dict());
93        Ok(map)
94    }
95}
96
97impl StorageArgs {
98    pub async fn run(self) -> Result<()> {
99        let config = self.load_config()?;
100
101        let Self { address, base_slot, offset, block, build, .. } = self;
102        let provider = utils::get_provider(&config)?;
103        let address = address.resolve(&provider).await?;
104
105        // Slot was provided, perform a simple RPC call
106        if let Some(slot) = base_slot {
107            let slot = Into::<U256>::into(slot).saturating_add(offset);
108            sh_println!(
109                "{}",
110                B256::from(
111                    provider
112                        .get_storage_at(address, slot)
113                        .block_id(block.unwrap_or_default())
114                        .await?
115                )
116            )?;
117            return Ok(());
118        }
119
120        // No slot was provided: get deployed bytecode at given address
121        let address_code =
122            provider.get_code_at(address).block_id(block.unwrap_or_default()).await?;
123        if address_code.is_empty() {
124            eyre::bail!("Provided address has no deployed code and thus no storage");
125        }
126
127        // The layout comes from `--proxy` when provided, the values always from `address`.
128        let source_address = match self.proxy {
129            Some(proxy) => proxy.resolve(&provider).await?,
130            None => address,
131        };
132        let source_code = if source_address == address {
133            address_code
134        } else {
135            provider.get_code_at(source_address).block_id(block.unwrap_or_default()).await?
136        };
137        if source_code.is_empty() {
138            eyre::bail!("Provided proxy address has no deployed code and thus no storage layout");
139        }
140
141        // Check if we're in a forge project and if we can find the source's code.
142        let project = build.project()?;
143        if project.paths.has_input_files()
144            && let Some(artifact) =
145                compile_local_storage_layout(&project, &source_code, shell::is_json())?
146        {
147            return fetch_and_print_storage(provider, address, block, &artifact).await;
148        }
149
150        let chain = utils::get_chain(config.chain, &provider).await?;
151        let client = match config.get_etherscan_config_with_chain(Some(chain))? {
152            Some(etherscan_config) => {
153                etherscan_config.into_client_with_no_proxy(config.eth_rpc_no_proxy)?
154            }
155            None => {
156                let api_key = self.etherscan.key().ok_or_else(|| {
157                    eyre::eyre!("You must provide an Etherscan API key if you're fetching a remote contract's storage.")
158                })?;
159                foundry_block_explorers::Client::new(chain, api_key)?
160            }
161        };
162        let source = find_source(client, source_address).await?;
163        let metadata = source.items.first().unwrap();
164        if metadata.is_vyper() {
165            eyre::bail!("Contract at provided address is not a valid Solidity contract");
166        }
167
168        // Create or reuse a persistent cache for Etherscan sources; fall back to a temp dir.
169        let mut root_path = Config::foundry_etherscan_chain_cache_dir(chain)
170            .map(|cache_root| cache_root.join("sources").join(address.to_string()));
171        if let Some(path) = &root_path
172            && let Err(err) = std::fs::create_dir_all(path)
173        {
174            sh_warn!("Could not create etherscan cache dir, falling back to temp: {err}")?;
175            root_path = None;
176        }
177        let _temp_dir;
178        let root_path = match root_path {
179            Some(path) => path,
180            None => {
181                _temp_dir = tempfile::tempdir()?;
182                _temp_dir.path().to_path_buf()
183            }
184        };
185        let mut project = etherscan_project(metadata, &root_path)?;
186        add_storage_layout_output(&mut project);
187
188        // Decide on compiler to use (user override -> metadata -> autodetect).
189        let meta_version = metadata.compiler_version()?;
190        let auto_detect = self.solc_version.is_none() && meta_version < MIN_SOLC;
191        project.compiler.solc = Some(match self.solc_version {
192            Some(user_version) => {
193                if user_version < MIN_SOLC {
194                    sh_warn!(
195                        "The provided --solc-version is {user_version} while the minimum version for \
196                         storage layouts is {MIN_SOLC} and as a result the output may be empty."
197                    )?;
198                }
199                SolcCompiler::Specific(Solc::find_or_install(&user_version)?)
200            }
201            None if auto_detect => SolcCompiler::AutoDetect,
202            None => SolcCompiler::Specific(Solc::find_or_install(&meta_version)?),
203        });
204
205        let find_artifact = |out: &ProjectCompileOutput| {
206            out.artifacts()
207                .find(|(name, _)| name == &metadata.contract_name)
208                .map(|(_, artifact)| artifact.clone())
209                .ok_or_else(|| eyre::eyre!("Could not find artifact"))
210        };
211        let out = ProjectCompiler::new().quiet(true).compile(&project)?;
212        let mut artifact = find_artifact(&out)?;
213        if auto_detect && artifact.storage_layout.as_ref().is_none_or(|l| l.storage.is_empty()) {
214            // Try recompiling with the minimum version.
215            sh_warn!(
216                "The requested contract was compiled with {meta_version} while the minimum version \
217                 for storage layouts is {MIN_SOLC} and as a result the output may be empty.",
218            )?;
219            project.compiler.solc = Some(SolcCompiler::Specific(Solc::find_or_install(&MIN_SOLC)?));
220            if let Ok(out) = ProjectCompiler::new().quiet(true).compile(&project) {
221                artifact = find_artifact(&out)?;
222            }
223        }
224
225        fetch_and_print_storage(provider, address, block, &artifact).await
226    }
227}
228
229/// Finds the local artifact matching `address_code` and produces its storage layout.
230///
231/// Human-readable output compiles only the target's source and imports when safe. JSON and unsafe
232/// cases retain the full-project compile to preserve existing behavior.
233fn compile_local_storage_layout(
234    project: &Project,
235    address_code: &Bytes,
236    json: bool,
237) -> Result<Option<ConfigurableContractArtifact>> {
238    // The JSON output exposes compiler-assigned AST IDs, which change when the compilation unit is
239    // reduced to the target's dependency graph. Preserve those IDs by retaining the full compile.
240    let full_compile = json
241        || project.build_info
242        || !project.cache_path().is_file()
243        || !project.paths.artifacts.is_dir();
244    if !full_compile {
245        let output = ProjectCompiler::new().quiet(false).compile(project)?;
246        let Some((target, artifact)) = find_unique_artifact(project, output, address_code)? else {
247            return Ok(None);
248        };
249        if artifact.storage_layout.is_some() {
250            return Ok(Some(artifact));
251        }
252        if let Ok(output) = compile_target_storage_layout(project, &target)
253            && let Some(artifact) = find_target_artifact(output, &target, address_code)
254        {
255            return Ok(Some(artifact));
256        }
257    }
258
259    let output = compile_full_storage_layout(project, json)?;
260    Ok(find_unique_artifact(project, output, address_code)?.map(|(_, artifact)| artifact))
261}
262
263/// Returns whether `code` is the artifact's deployed bytecode.
264///
265/// Immutables are zero in the artifact and only filled in at deployment, so their values are
266/// ignored, but only when the artifact ends in a metadata hash. The hash commits to the sources,
267/// so the rest of the code still has to come from the same contract.
268fn has_deployed_code(artifact: &ConfigurableContractArtifact, code: &Bytes) -> bool {
269    let Some(deployed_code) = artifact.get_deployed_bytecode_bytes() else { return false };
270    if deployed_code[..] == code[..] {
271        return true;
272    }
273    if deployed_code.len() != code.len() || !has_metadata_hash(&deployed_code) {
274        return false;
275    }
276
277    let mut code = code.to_vec();
278    let immutables =
279        artifact.deployed_bytecode.iter().flat_map(|b| b.immutable_references.values());
280    for offsets in immutables.flatten() {
281        let range = offsets.start as usize..offsets.start as usize + offsets.length as usize;
282        let (Some(value), Some(placeholder)) =
283            (code.get_mut(range.clone()), deployed_code.get(range))
284        else {
285            return false;
286        };
287        value.copy_from_slice(placeholder);
288    }
289    deployed_code[..] == code[..]
290}
291
292fn find_target_artifact(
293    output: ProjectCompileOutput,
294    target: &ArtifactId,
295    address_code: &Bytes,
296) -> Option<ConfigurableContractArtifact> {
297    output.into_artifacts().find_map(|(id, artifact)| {
298        (same_artifact(&id, target)
299            && artifact.storage_layout.is_some()
300            && has_deployed_code(&artifact, address_code))
301        .then_some(artifact)
302    })
303}
304
305/// Returns the artifact whose deployed bytecode is `code`.
306///
307/// Fails if the code matches more than one contract, since each can have a different storage
308/// layout.
309fn find_unique_artifact(
310    project: &Project,
311    output: ProjectCompileOutput,
312    code: &Bytes,
313) -> Result<Option<(ArtifactId, ConfigurableContractArtifact)>> {
314    let mut found = None;
315    let mut contracts = BTreeSet::new();
316    for (id, artifact) in output.into_artifacts() {
317        if has_deployed_code(&artifact, code) {
318            contracts.insert(id.clone().with_stripped_file_prefixes(project.root()).identifier());
319            found.get_or_insert((id, artifact));
320        }
321    }
322    if contracts.len() > 1 {
323        let contracts = contracts.into_iter().collect::<Vec<_>>().join(", ");
324        eyre::bail!("Deployed code matches multiple local contracts: {contracts}");
325    }
326    Ok(found)
327}
328
329fn compile_target_storage_layout(
330    project: &Project,
331    target: &ArtifactId,
332) -> Result<ProjectCompileOutput> {
333    let mut project = project.clone();
334    project.no_artifacts = true;
335    add_storage_layout_output(&mut project);
336    ProjectCompiler::new().quiet(true).files([target.source.clone()]).compile(&project)
337}
338
339fn compile_full_storage_layout(project: &Project, quiet: bool) -> Result<ProjectCompileOutput> {
340    let mut project = project.clone();
341    add_storage_layout_output(&mut project);
342    ProjectCompiler::new().quiet(quiet).compile(&project)
343}
344
345/// Returns whether two artifact IDs identify the same contract across compiler runs.
346///
347/// Changing the output selection changes the build ID, and compiling fewer files can change an
348/// artifact path that was disambiguated due to a name collision. Neither can be used to match the
349/// normal compile against the targeted storage-layout compile.
350fn same_artifact(left: &ArtifactId, right: &ArtifactId) -> bool {
351    left.name == right.name
352        && left.source == right.source
353        && left.version == right.version
354        && left.profile == right.profile
355}
356
357/// Represents the value of a storage slot `eth_getStorageAt` call.
358#[derive(Clone, Debug, PartialEq, Eq)]
359struct StorageValue {
360    /// The slot number.
361    slot: B256,
362    /// The value as returned by `eth_getStorageAt`.
363    raw_slot_value: B256,
364}
365
366impl StorageValue {
367    /// Returns the value of the storage slot, applying the offset if necessary.
368    fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
369        let offset = offset as usize;
370        let end = number_of_bytes.map_or(32, |n| (offset + n).min(32));
371        // Reverse range, because the value is stored in big endian.
372        B256::left_padding_from(&self.raw_slot_value[32 - end..32 - offset])
373    }
374}
375
376/// Represents the storage layout of a contract and its values.
377#[derive(Clone, Debug, Serialize, Deserialize)]
378struct StorageReport {
379    #[serde(flatten)]
380    layout: StorageLayout,
381    values: Vec<B256>,
382}
383
384async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
385    provider: P,
386    address: Address,
387    block: Option<BlockId>,
388    artifact: &ConfigurableContractArtifact,
389) -> Result<()> {
390    let Some(layout) = artifact.storage_layout.as_ref().filter(|l| !l.storage.is_empty()) else {
391        sh_warn!("Storage layout is empty.")?;
392        return Ok(());
393    };
394    let values = futures::future::try_join_all(layout.storage.iter().map(|storage_slot| async {
395        let slot = B256::from(U256::from_str(&storage_slot.slot)?);
396        let raw_slot_value = provider
397            .get_storage_at(address, slot.into())
398            .block_id(block.unwrap_or_default())
399            .await?;
400        let storage_type = layout.types.get(&storage_slot.storage_type);
401        let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() }.value(
402            storage_slot.offset,
403            storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
404        );
405        Ok::<_, eyre::Report>(value)
406    }))
407    .await?;
408
409    if shell::is_json() {
410        let report = StorageReport { layout: layout.clone(), values };
411        sh_println!("{}", serde_json::to_string_pretty(&serde_json::to_value(report)?)?)?;
412        return Ok(());
413    }
414
415    let mut table = Table::new();
416    table.load_style(if shell::is_markdown() {
417        ASCII_MARKDOWN
418    } else {
419        ASCII_FULL.with_rounded_corners()
420    });
421    table.set_header(["Name", "Type", "Slot", "Offset", "Bytes", "Value", "Hex Value", "Contract"]);
422    for (slot, value) in layout.storage.iter().zip(values) {
423        let storage_type = layout.types.get(&slot.storage_type);
424        table.add_row([
425            slot.label.as_str(),
426            storage_type.map_or("?", |t| &t.label),
427            &slot.slot,
428            &slot.offset.to_string(),
429            storage_type.map_or("?", |t| &t.number_of_bytes),
430            &Into::<U256>::into(value).to_string(),
431            &value.to_string(),
432            &slot.contract,
433        ]);
434    }
435    sh_println!("\n{table}\n")?;
436    Ok(())
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use foundry_compilers::{PathStyle, artifacts::BytecodeHash};
443    use foundry_config::{CompilationRestrictions, SettingsOverrides, filter::GlobMatcher};
444    use foundry_test_utils::{
445        TestProject,
446        util::{OTHER_SOLC_VERSION, SOLC_VERSION},
447    };
448    use std::path::Path;
449
450    fn load_project(project: &TestProject) -> Project {
451        load_project_with_config(project, Config::with_root(project.root()))
452    }
453
454    fn load_project_with_config(project: &TestProject, config: Config) -> Project {
455        let project = config.canonic_at(project.root()).project().unwrap();
456        assert!(project.paths.has_input_files(), "{:?}", project.paths);
457        project
458    }
459
460    /// Compiles `project` and returns the artifact id and deployed code of `name` in `source`.
461    fn compile_target(project: &Project, source: &Path, name: &str) -> (ArtifactId, Bytes) {
462        let output = ProjectCompiler::new().quiet(true).compile(project).unwrap();
463        let (target, artifact) =
464            output.artifact_ids().find(|(id, _)| id.source == source && id.name == name).unwrap();
465        (target, artifact.get_deployed_bytecode_bytes().unwrap().into_owned())
466    }
467
468    #[test]
469    fn local_storage_layout_targets_exact_artifact_and_imports() {
470        let prj = TestProject::new("cast-storage-target", PathStyle::Dapptools);
471        let base_path = prj.add_source("Base", "contract Base { uint256 baseValue; }");
472        let unrelated_path = prj.add_source("Target", "contract Target { uint256 unrelated; }");
473        let target_path = prj.add_source(
474            "nested/Target",
475            r#"
476import "src/Base.sol";
477
478contract Target is Base {
479    uint256 value;
480
481    function marker() external pure returns (bool) {
482        return true;
483    }
484}
485"#,
486        );
487        let project = load_project(&prj);
488        let (target, address_code) = compile_target(&project, &target_path, "Target");
489        let artifact_before = std::fs::read(&target.path).unwrap();
490        let cache_before = std::fs::read(project.cache_path()).unwrap();
491
492        let output = compile_target_storage_layout(&project, &target).unwrap();
493        assert!(output.artifact_ids().any(|(id, _)| id.source == base_path));
494        assert!(!output.artifact_ids().any(|(id, _)| id.source == unrelated_path));
495        let artifact = find_target_artifact(output, &target, &address_code).unwrap();
496        let labels = artifact
497            .storage_layout
498            .as_ref()
499            .unwrap()
500            .storage
501            .iter()
502            .map(|slot| slot.label.as_str())
503            .collect::<Vec<_>>();
504        assert_eq!(labels, ["baseValue", "value"]);
505
506        let artifact =
507            compile_local_storage_layout(&project, &address_code, false).unwrap().unwrap();
508        assert!(artifact.storage_layout.is_some());
509        assert_eq!(std::fs::read(&target.path).unwrap(), artifact_before);
510        assert_eq!(std::fs::read(project.cache_path()).unwrap(), cache_before);
511    }
512
513    #[test]
514    fn local_storage_layout_rechecks_bytecode_after_source_change() {
515        let prj = TestProject::new("cast-storage-source-change", PathStyle::Dapptools);
516        let target_path = prj.add_source("Target", "contract Target { uint256 originalValue; }");
517        let project = load_project(&prj);
518        let (target, address_code) = compile_target(&project, &target_path, "Target");
519
520        std::fs::write(
521            &target_path,
522            format!(
523                "// SPDX-License-Identifier: MIT\npragma solidity ={SOLC_VERSION};\ncontract Target {{ uint256 changedValue; }}\n"
524            ),
525        )
526        .unwrap();
527
528        let output = compile_target_storage_layout(&project, &target).unwrap();
529        assert!(find_target_artifact(output, &target, &address_code).is_none());
530        assert!(compile_local_storage_layout(&project, &address_code, false).unwrap().is_none());
531    }
532
533    #[test]
534    fn local_storage_layout_preserves_compiler_profile() {
535        let prj = TestProject::new("cast-storage-profile", PathStyle::Dapptools);
536        let target_path = prj.add_source("Profiled", "contract Profiled { uint256 value; }");
537        let mut config = Config::with_root(prj.root());
538        config.additional_compiler_profiles = vec![SettingsOverrides {
539            name: "optimized".to_string(),
540            via_ir: Some(true),
541            evm_version: None,
542            optimizer: Some(true),
543            optimizer_runs: Some(1),
544            bytecode_hash: None,
545        }];
546        config.compilation_restrictions = vec![CompilationRestrictions {
547            paths: GlobMatcher::from_str("src/Profiled.sol").unwrap(),
548            version: None,
549            via_ir: Some(true),
550            bytecode_hash: None,
551            min_optimizer_runs: None,
552            optimizer_runs: Some(1),
553            max_optimizer_runs: None,
554            min_evm_version: None,
555            evm_version: None,
556            max_evm_version: None,
557        }];
558        let project = load_project_with_config(&prj, config);
559        let (target, address_code) = compile_target(&project, &target_path, "Profiled");
560        assert_eq!(target.profile, "optimized");
561
562        let output = compile_target_storage_layout(&project, &target).unwrap();
563        let (compiled, _) = output
564            .artifact_ids()
565            .find(|(id, artifact)| {
566                same_artifact(id, &target) && has_deployed_code(artifact, &address_code)
567            })
568            .unwrap();
569        assert_eq!(compiled.version, target.version);
570        assert_eq!(compiled.profile, target.profile);
571        assert!(find_target_artifact(output, &target, &address_code).is_some());
572    }
573
574    #[test]
575    fn local_storage_layout_preserves_compiler_version_in_multi_version_project() {
576        let prj = TestProject::new("cast-storage-multi-version", PathStyle::Dapptools);
577        let old_path = prj.add_raw_source(
578            "Old",
579            &format!(
580                "// SPDX-License-Identifier: MIT\npragma solidity ={OTHER_SOLC_VERSION};\ncontract Old {{ uint256 oldValue; }}\n"
581            ),
582        );
583        let new_path = prj.add_raw_source(
584            "New",
585            &format!(
586                "// SPDX-License-Identifier: MIT\npragma solidity ={SOLC_VERSION};\ncontract New {{ uint256 newValue; }}\n"
587            ),
588        );
589        let mut config = Config::with_root(prj.root());
590        config.solc = None;
591        let project = load_project_with_config(&prj, config);
592        let (target, address_code) = compile_target(&project, &old_path, "Old");
593        assert_eq!(target.version, Version::parse(OTHER_SOLC_VERSION).unwrap());
594
595        let output = compile_target_storage_layout(&project, &target).unwrap();
596        assert!(!output.artifact_ids().any(|(id, _)| id.source == new_path));
597        assert!(find_target_artifact(output, &target, &address_code).is_some());
598        let artifact =
599            compile_local_storage_layout(&project, &address_code, false).unwrap().unwrap();
600        assert_eq!(artifact.storage_layout.unwrap().storage[0].label, "oldValue");
601    }
602
603    #[test]
604    fn local_storage_layout_preserves_full_json_ast_ids() {
605        let prj = TestProject::new("cast-storage-json-ast-ids", PathStyle::Dapptools);
606        prj.add_source("First", "contract First { uint256 first; }");
607        let target_path = prj.add_source("Target", "contract Target { uint256 value; }");
608        let project = load_project(&prj);
609        let (target, address_code) = compile_target(&project, &target_path, "Target");
610
611        let targeted = find_target_artifact(
612            compile_target_storage_layout(&project, &target).unwrap(),
613            &target,
614            &address_code,
615        )
616        .unwrap();
617        let full = compile_local_storage_layout(&project, &address_code, true).unwrap().unwrap();
618
619        assert_ne!(full.storage_layout, targeted.storage_layout);
620        assert_eq!(full.storage_layout.unwrap().storage[0].label, "value");
621    }
622
623    #[test]
624    fn local_storage_layout_uses_full_compile_with_build_info() {
625        let prj = TestProject::new("cast-storage-build-info", PathStyle::Dapptools);
626        let target_path = prj.add_source("Target", "contract Target { uint256 value; }");
627        let mut config = Config::with_root(prj.root());
628        config.build_info = true;
629        let project = load_project_with_config(&prj, config);
630        let (_, address_code) = compile_target(&project, &target_path, "Target");
631
632        let artifact =
633            compile_local_storage_layout(&project, &address_code, true).unwrap().unwrap();
634        assert!(artifact.storage_layout.is_some());
635    }
636
637    #[test]
638    fn local_storage_layout_uses_full_compile_without_cache() {
639        let prj = TestProject::new("cast-storage-no-cache", PathStyle::Dapptools);
640        let target_path = prj.add_source("Target", "contract Target { uint256 value; }");
641        let project = load_project(&prj);
642        let mut code_project = project.clone();
643        code_project.no_artifacts = true;
644        let (_, address_code) = compile_target(&code_project, &target_path, "Target");
645        assert!(!project.cache_path().exists());
646
647        let artifact =
648            compile_local_storage_layout(&project, &address_code, true).unwrap().unwrap();
649        assert!(artifact.storage_layout.is_some());
650    }
651
652    #[test]
653    fn local_storage_layout_ignores_immutable_values() {
654        let prj = TestProject::new("cast-storage-immutable", PathStyle::Dapptools);
655        let target_path = prj.add_source(
656            "Pinned",
657            r#"
658contract Pinned {
659    address public immutable token;
660    uint256 count;
661
662    constructor(address token_) {
663        token = token_;
664    }
665}
666"#,
667        );
668        let project = load_project(&prj);
669        let output = ProjectCompiler::new().quiet(true).compile(&project).unwrap();
670        let (_, artifact) = output.artifact_ids().find(|(id, _)| id.source == target_path).unwrap();
671
672        // Deploy-time code with `token` set to `address(1)`.
673        let mut code = artifact.get_deployed_bytecode_bytes().unwrap().to_vec();
674        let immutables = &artifact.deployed_bytecode.as_ref().unwrap().immutable_references;
675        assert!(!immutables.is_empty());
676        for offsets in immutables.values().flatten() {
677            code[(offsets.start + offsets.length - 1) as usize] = 1;
678        }
679        let address_code = Bytes::from(code.clone());
680        assert!(has_deployed_code(artifact, &address_code));
681
682        for json in [false, true] {
683            let artifact =
684                compile_local_storage_layout(&project, &address_code, json).unwrap().unwrap();
685            let labels = artifact
686                .storage_layout
687                .as_ref()
688                .unwrap()
689                .storage
690                .iter()
691                .map(|slot| slot.label.as_str())
692                .collect::<Vec<_>>();
693            assert_eq!(labels, ["count"]);
694        }
695
696        code[0] ^= 1;
697        assert!(!has_deployed_code(artifact, &Bytes::from(code)));
698    }
699
700    #[test]
701    fn local_storage_layout_requires_metadata_hash_for_immutables() {
702        let prj = TestProject::new("cast-storage-immutable-no-hash", PathStyle::Dapptools);
703        let target_path = prj.add_source(
704            "Pinned",
705            r#"
706contract Pinned {
707    address public immutable token;
708    uint256 count;
709
710    constructor(address token_) {
711        token = token_;
712    }
713}
714"#,
715        );
716
717        for cbor_metadata in [true, false] {
718            let mut config = Config::with_root(prj.root());
719            config.bytecode_hash = BytecodeHash::None;
720            config.cbor_metadata = cbor_metadata;
721            let project = load_project_with_config(&prj, config);
722            let output = ProjectCompiler::new().quiet(true).compile(&project).unwrap();
723            let (_, artifact) =
724                output.artifact_ids().find(|(id, _)| id.source == target_path).unwrap();
725            let mut code = artifact.get_deployed_bytecode_bytes().unwrap().to_vec();
726            assert!(has_deployed_code(artifact, &Bytes::from(code.clone())));
727
728            // Without a metadata hash, a different contract could share every other byte.
729            let immutables = &artifact.deployed_bytecode.as_ref().unwrap().immutable_references;
730            for offsets in immutables.values().flatten() {
731                code[(offsets.start + offsets.length - 1) as usize] = 1;
732            }
733            let address_code = Bytes::from(code);
734            assert!(!has_deployed_code(artifact, &address_code));
735            for json in [false, true] {
736                assert!(
737                    compile_local_storage_layout(&project, &address_code, json).unwrap().is_none()
738                );
739            }
740        }
741    }
742
743    #[test]
744    fn local_storage_layout_rejects_ambiguous_artifacts() {
745        let prj = TestProject::new("cast-storage-ambiguous", PathStyle::Dapptools);
746        let first_path = prj.add_source("First", "contract First { uint256 first; }");
747        let second_path = prj.add_source("Second", "contract Second { uint256 second; }");
748        let mut config = Config::with_root(prj.root());
749        config.bytecode_hash = BytecodeHash::None;
750        config.cbor_metadata = false;
751        let project = load_project_with_config(&prj, config);
752        let (_, address_code) = compile_target(&project, &first_path, "First");
753        assert_eq!(compile_target(&project, &second_path, "Second").1, address_code);
754
755        for json in [false, true] {
756            let err = compile_local_storage_layout(&project, &address_code, json).unwrap_err();
757            assert_eq!(
758                err.to_string(),
759                "Deployed code matches multiple local contracts: src/First.sol:First, src/Second.sol:Second"
760            );
761        }
762    }
763
764    #[test]
765    fn parse_storage_etherscan_api_key() {
766        let args =
767            StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
768        assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
769
770        unsafe {
771            std::env::set_var("ETHERSCAN_API_KEY", "FXY");
772        }
773        let config = args.load_config().unwrap();
774        unsafe {
775            std::env::remove_var("ETHERSCAN_API_KEY");
776        }
777        assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
778        assert_eq!(config.get_etherscan_api_key(None).unwrap(), "dummykey".to_string());
779    }
780}