Skip to main content

cast/cmd/
storage.rs

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