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    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::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 = U256::from_be_bytes(slot.0).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        // Check if we're in a forge project and if we can find the address' code
128        let project = build.project()?;
129        if project.paths.has_input_files()
130            && let Some(artifact) =
131                compile_local_storage_layout(&project, &address_code, shell::is_json())?
132        {
133            return fetch_and_print_storage(provider, address, block, &artifact).await;
134        }
135
136        let chain = utils::get_chain(config.chain, &provider).await?;
137        let client = match config.get_etherscan_config_with_chain(Some(chain))? {
138            Some(etherscan_config) => {
139                etherscan_config.into_client_with_no_proxy(config.eth_rpc_no_proxy)?
140            }
141            None => {
142                let api_key = self.etherscan.key().ok_or_else(|| {
143                    eyre::eyre!("You must provide an Etherscan API key if you're fetching a remote contract's storage.")
144                })?;
145                foundry_block_explorers::Client::new(chain, api_key)?
146            }
147        };
148        let source_address = match self.proxy {
149            Some(proxy) => proxy.resolve(&provider).await?,
150            None => address,
151        };
152        let source = find_source(client, source_address).await?;
153        let metadata = source.items.first().unwrap();
154        if metadata.is_vyper() {
155            eyre::bail!("Contract at provided address is not a valid Solidity contract");
156        }
157
158        // Create or reuse a persistent cache for Etherscan sources; fall back to a temp dir.
159        let mut root_path = Config::foundry_etherscan_chain_cache_dir(chain)
160            .map(|cache_root| cache_root.join("sources").join(address.to_string()));
161        if let Some(path) = &root_path
162            && let Err(err) = std::fs::create_dir_all(path)
163        {
164            sh_warn!("Could not create etherscan cache dir, falling back to temp: {err}")?;
165            root_path = None;
166        }
167        let _temp_dir;
168        let root_path = match root_path {
169            Some(path) => path,
170            None => {
171                _temp_dir = tempfile::tempdir()?;
172                _temp_dir.path().to_path_buf()
173            }
174        };
175        let mut project = etherscan_project(metadata, &root_path)?;
176        add_storage_layout_output(&mut project);
177
178        // Decide on compiler to use (user override -> metadata -> autodetect).
179        let meta_version = metadata.compiler_version()?;
180        let auto_detect = self.solc_version.is_none() && meta_version < MIN_SOLC;
181        project.compiler.solc = Some(match self.solc_version {
182            Some(user_version) => {
183                if user_version < MIN_SOLC {
184                    sh_warn!(
185                        "The provided --solc-version is {user_version} while the minimum version for \
186                         storage layouts is {MIN_SOLC} and as a result the output may be empty."
187                    )?;
188                }
189                SolcCompiler::Specific(Solc::find_or_install(&user_version)?)
190            }
191            None if auto_detect => SolcCompiler::AutoDetect,
192            None => SolcCompiler::Specific(Solc::find_or_install(&meta_version)?),
193        });
194
195        let find_artifact = |out: &ProjectCompileOutput| {
196            out.artifacts()
197                .find(|(name, _)| name == &metadata.contract_name)
198                .map(|(_, artifact)| artifact.clone())
199                .ok_or_else(|| eyre::eyre!("Could not find artifact"))
200        };
201        let out = ProjectCompiler::new().quiet(true).compile(&project)?;
202        let mut artifact = find_artifact(&out)?;
203        if auto_detect && artifact.storage_layout.as_ref().is_none_or(|l| l.storage.is_empty()) {
204            // Try recompiling with the minimum version.
205            sh_warn!(
206                "The requested contract was compiled with {meta_version} while the minimum version \
207                 for storage layouts is {MIN_SOLC} and as a result the output may be empty.",
208            )?;
209            project.compiler.solc = Some(SolcCompiler::Specific(Solc::find_or_install(&MIN_SOLC)?));
210            if let Ok(out) = ProjectCompiler::new().quiet(true).compile(&project) {
211                artifact = find_artifact(&out)?;
212            }
213        }
214
215        fetch_and_print_storage(provider, address, block, &artifact).await
216    }
217}
218
219/// Finds the local artifact matching `address_code` and produces its storage layout.
220///
221/// Human-readable output compiles only the target's source and imports when safe. JSON and unsafe
222/// cases retain the full-project compile to preserve existing behavior.
223fn compile_local_storage_layout(
224    project: &Project,
225    address_code: &Bytes,
226    json: bool,
227) -> Result<Option<ConfigurableContractArtifact>> {
228    // The JSON output exposes compiler-assigned AST IDs, which change when the compilation unit is
229    // reduced to the target's dependency graph. Preserve those IDs by retaining the full compile.
230    let full_compile = json
231        || project.build_info
232        || !project.cache_path().is_file()
233        || !project.paths.artifacts.is_dir();
234    if !full_compile {
235        let output = ProjectCompiler::new().quiet(false).compile(project)?;
236        let Some((target, artifact)) =
237            output.into_artifacts().find(|(_, artifact)| has_deployed_code(artifact, address_code))
238        else {
239            return Ok(None);
240        };
241        if artifact.storage_layout.is_some() {
242            return Ok(Some(artifact));
243        }
244        if let Ok(output) = compile_target_storage_layout(project, &target)
245            && let Some(artifact) = find_target_artifact(output, &target, address_code)
246        {
247            return Ok(Some(artifact));
248        }
249    }
250
251    let output = compile_full_storage_layout(project, json)?;
252    Ok(output
253        .into_artifacts()
254        .find_map(|(_, artifact)| has_deployed_code(&artifact, address_code).then_some(artifact)))
255}
256
257fn has_deployed_code(artifact: &ConfigurableContractArtifact, code: &Bytes) -> bool {
258    artifact.get_deployed_bytecode_bytes().as_deref() == Some(code)
259}
260
261fn find_target_artifact(
262    output: ProjectCompileOutput,
263    target: &ArtifactId,
264    address_code: &Bytes,
265) -> Option<ConfigurableContractArtifact> {
266    output.into_artifacts().find_map(|(id, artifact)| {
267        (same_artifact(&id, target)
268            && artifact.storage_layout.is_some()
269            && has_deployed_code(&artifact, address_code))
270        .then_some(artifact)
271    })
272}
273
274fn compile_target_storage_layout(
275    project: &Project,
276    target: &ArtifactId,
277) -> Result<ProjectCompileOutput> {
278    let mut project = project.clone();
279    project.no_artifacts = true;
280    add_storage_layout_output(&mut project);
281    ProjectCompiler::new().quiet(true).files([target.source.clone()]).compile(&project)
282}
283
284fn compile_full_storage_layout(project: &Project, quiet: bool) -> Result<ProjectCompileOutput> {
285    let mut project = project.clone();
286    add_storage_layout_output(&mut project);
287    ProjectCompiler::new().quiet(quiet).compile(&project)
288}
289
290/// Returns whether two artifact IDs identify the same contract across compiler runs.
291///
292/// Changing the output selection changes the build ID, and compiling fewer files can change an
293/// artifact path that was disambiguated due to a name collision. Neither can be used to match the
294/// normal compile against the targeted storage-layout compile.
295fn same_artifact(left: &ArtifactId, right: &ArtifactId) -> bool {
296    left.name == right.name
297        && left.source == right.source
298        && left.version == right.version
299        && left.profile == right.profile
300}
301
302/// Represents the value of a storage slot `eth_getStorageAt` call.
303#[derive(Clone, Debug, PartialEq, Eq)]
304struct StorageValue {
305    /// The slot number.
306    slot: B256,
307    /// The value as returned by `eth_getStorageAt`.
308    raw_slot_value: B256,
309}
310
311impl StorageValue {
312    /// Returns the value of the storage slot, applying the offset if necessary.
313    fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
314        let offset = offset as usize;
315        let end = number_of_bytes.map_or(32, |n| (offset + n).min(32));
316        // Reverse range, because the value is stored in big endian.
317        B256::left_padding_from(&self.raw_slot_value[32 - end..32 - offset])
318    }
319}
320
321/// Represents the storage layout of a contract and its values.
322#[derive(Clone, Debug, Serialize, Deserialize)]
323struct StorageReport {
324    #[serde(flatten)]
325    layout: StorageLayout,
326    values: Vec<B256>,
327}
328
329async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
330    provider: P,
331    address: Address,
332    block: Option<BlockId>,
333    artifact: &ConfigurableContractArtifact,
334) -> Result<()> {
335    let Some(layout) = artifact.storage_layout.as_ref().filter(|l| !l.storage.is_empty()) else {
336        sh_warn!("Storage layout is empty.")?;
337        return Ok(());
338    };
339    let values = futures::future::try_join_all(layout.storage.iter().map(|storage_slot| async {
340        let slot = B256::from(U256::from_str(&storage_slot.slot)?);
341        let raw_slot_value = provider
342            .get_storage_at(address, slot.into())
343            .block_id(block.unwrap_or_default())
344            .await?;
345        let storage_type = layout.types.get(&storage_slot.storage_type);
346        let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() }.value(
347            storage_slot.offset,
348            storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
349        );
350        Ok::<_, eyre::Report>(value)
351    }))
352    .await?;
353
354    if shell::is_json() {
355        let report = StorageReport { layout: layout.clone(), values };
356        sh_println!("{}", serde_json::to_string_pretty(&serde_json::to_value(report)?)?)?;
357        return Ok(());
358    }
359
360    let mut table = Table::new();
361    table.load_style(if shell::is_markdown() {
362        ASCII_MARKDOWN
363    } else {
364        ASCII_FULL.with_rounded_corners()
365    });
366    table.set_header(["Name", "Type", "Slot", "Offset", "Bytes", "Value", "Hex Value", "Contract"]);
367    for (slot, value) in layout.storage.iter().zip(values) {
368        let storage_type = layout.types.get(&slot.storage_type);
369        table.add_row([
370            slot.label.as_str(),
371            storage_type.map_or("?", |t| &t.label),
372            &slot.slot,
373            &slot.offset.to_string(),
374            storage_type.map_or("?", |t| &t.number_of_bytes),
375            &U256::from_be_bytes(value.0).to_string(),
376            &value.to_string(),
377            &slot.contract,
378        ]);
379    }
380    sh_println!("\n{table}\n")?;
381    Ok(())
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use foundry_compilers::PathStyle;
388    use foundry_config::{CompilationRestrictions, SettingsOverrides, filter::GlobMatcher};
389    use foundry_test_utils::{
390        TestProject,
391        util::{OTHER_SOLC_VERSION, SOLC_VERSION},
392    };
393    use std::path::Path;
394
395    fn test_project(name: &str) -> TestProject {
396        let project = TestProject::new(name, PathStyle::Dapptools);
397        foundry_test_utils::util::initialize(project.root());
398        project
399    }
400
401    fn load_project(project: &TestProject) -> Project {
402        load_project_with_config(project, Config::with_root(project.root()))
403    }
404
405    fn load_project_with_config(project: &TestProject, config: Config) -> Project {
406        let project = config.canonic_at(project.root()).project().unwrap();
407        assert!(project.paths.has_input_files(), "{:?}", project.paths);
408        project
409    }
410
411    /// Compiles `project` and returns the artifact id and deployed code of `name` in `source`.
412    fn compile_target(project: &Project, source: &Path, name: &str) -> (ArtifactId, Bytes) {
413        let output = ProjectCompiler::new().quiet(true).compile(project).unwrap();
414        let (target, artifact) =
415            output.artifact_ids().find(|(id, _)| id.source == source && id.name == name).unwrap();
416        (target, artifact.get_deployed_bytecode_bytes().unwrap().into_owned())
417    }
418
419    #[test]
420    fn local_storage_layout_targets_exact_artifact_and_imports() {
421        let prj = test_project("cast-storage-target");
422        let base_path = prj.add_source("Base", "contract Base { uint256 baseValue; }");
423        let unrelated_path = prj.add_source("Target", "contract Target { uint256 unrelated; }");
424        let target_path = prj.add_source(
425            "nested/Target",
426            r#"
427import "src/Base.sol";
428
429contract Target is Base {
430    uint256 value;
431
432    function marker() external pure returns (bool) {
433        return true;
434    }
435}
436"#,
437        );
438        let project = load_project(&prj);
439        let (target, address_code) = compile_target(&project, &target_path, "Target");
440        let artifact_before = std::fs::read(&target.path).unwrap();
441        let cache_before = std::fs::read(project.cache_path()).unwrap();
442
443        let output = compile_target_storage_layout(&project, &target).unwrap();
444        assert!(output.artifact_ids().any(|(id, _)| id.source == base_path));
445        assert!(!output.artifact_ids().any(|(id, _)| id.source == unrelated_path));
446        let artifact = find_target_artifact(output, &target, &address_code).unwrap();
447        let labels = artifact
448            .storage_layout
449            .as_ref()
450            .unwrap()
451            .storage
452            .iter()
453            .map(|slot| slot.label.as_str())
454            .collect::<Vec<_>>();
455        assert_eq!(labels, ["baseValue", "value"]);
456
457        let artifact =
458            compile_local_storage_layout(&project, &address_code, false).unwrap().unwrap();
459        assert!(artifact.storage_layout.is_some());
460        assert_eq!(std::fs::read(&target.path).unwrap(), artifact_before);
461        assert_eq!(std::fs::read(project.cache_path()).unwrap(), cache_before);
462    }
463
464    #[test]
465    fn local_storage_layout_rechecks_bytecode_after_source_change() {
466        let prj = test_project("cast-storage-source-change");
467        let target_path = prj.add_source("Target", "contract Target { uint256 originalValue; }");
468        let project = load_project(&prj);
469        let (target, address_code) = compile_target(&project, &target_path, "Target");
470
471        std::fs::write(
472            &target_path,
473            format!(
474                "// SPDX-License-Identifier: MIT\npragma solidity ={SOLC_VERSION};\ncontract Target {{ uint256 changedValue; }}\n"
475            ),
476        )
477        .unwrap();
478
479        let output = compile_target_storage_layout(&project, &target).unwrap();
480        assert!(find_target_artifact(output, &target, &address_code).is_none());
481        assert!(compile_local_storage_layout(&project, &address_code, false).unwrap().is_none());
482    }
483
484    #[test]
485    fn local_storage_layout_preserves_compiler_profile() {
486        let prj = test_project("cast-storage-profile");
487        let target_path = prj.add_source("Profiled", "contract Profiled { uint256 value; }");
488        let mut config = Config::with_root(prj.root());
489        config.additional_compiler_profiles = vec![SettingsOverrides {
490            name: "optimized".to_string(),
491            via_ir: Some(true),
492            evm_version: None,
493            optimizer: Some(true),
494            optimizer_runs: Some(1),
495            bytecode_hash: None,
496        }];
497        config.compilation_restrictions = vec![CompilationRestrictions {
498            paths: GlobMatcher::from_str("src/Profiled.sol").unwrap(),
499            version: None,
500            via_ir: Some(true),
501            bytecode_hash: None,
502            min_optimizer_runs: None,
503            optimizer_runs: Some(1),
504            max_optimizer_runs: None,
505            min_evm_version: None,
506            evm_version: None,
507            max_evm_version: None,
508        }];
509        let project = load_project_with_config(&prj, config);
510        let (target, address_code) = compile_target(&project, &target_path, "Profiled");
511        assert_eq!(target.profile, "optimized");
512
513        let output = compile_target_storage_layout(&project, &target).unwrap();
514        let (compiled, _) = output
515            .artifact_ids()
516            .find(|(id, artifact)| {
517                same_artifact(id, &target) && has_deployed_code(artifact, &address_code)
518            })
519            .unwrap();
520        assert_eq!(compiled.version, target.version);
521        assert_eq!(compiled.profile, target.profile);
522        assert!(find_target_artifact(output, &target, &address_code).is_some());
523    }
524
525    #[test]
526    fn local_storage_layout_preserves_compiler_version_in_multi_version_project() {
527        let prj = test_project("cast-storage-multi-version");
528        let old_path = prj.add_raw_source(
529            "Old",
530            &format!(
531                "// SPDX-License-Identifier: MIT\npragma solidity ={OTHER_SOLC_VERSION};\ncontract Old {{ uint256 oldValue; }}\n"
532            ),
533        );
534        let new_path = prj.add_raw_source(
535            "New",
536            &format!(
537                "// SPDX-License-Identifier: MIT\npragma solidity ={SOLC_VERSION};\ncontract New {{ uint256 newValue; }}\n"
538            ),
539        );
540        let mut config = Config::with_root(prj.root());
541        config.solc = None;
542        let project = load_project_with_config(&prj, config);
543        let (target, address_code) = compile_target(&project, &old_path, "Old");
544        assert_eq!(target.version, Version::parse(OTHER_SOLC_VERSION).unwrap());
545
546        let output = compile_target_storage_layout(&project, &target).unwrap();
547        assert!(!output.artifact_ids().any(|(id, _)| id.source == new_path));
548        assert!(find_target_artifact(output, &target, &address_code).is_some());
549        let artifact =
550            compile_local_storage_layout(&project, &address_code, false).unwrap().unwrap();
551        assert_eq!(artifact.storage_layout.unwrap().storage[0].label, "oldValue");
552    }
553
554    #[test]
555    fn local_storage_layout_preserves_full_json_ast_ids() {
556        let prj = test_project("cast-storage-json-ast-ids");
557        prj.add_source("First", "contract First { uint256 first; }");
558        let target_path = prj.add_source("Target", "contract Target { uint256 value; }");
559        let project = load_project(&prj);
560        let (target, address_code) = compile_target(&project, &target_path, "Target");
561
562        let targeted = find_target_artifact(
563            compile_target_storage_layout(&project, &target).unwrap(),
564            &target,
565            &address_code,
566        )
567        .unwrap();
568        let full = compile_local_storage_layout(&project, &address_code, true).unwrap().unwrap();
569
570        assert_ne!(full.storage_layout, targeted.storage_layout);
571        assert_eq!(full.storage_layout.unwrap().storage[0].label, "value");
572    }
573
574    #[test]
575    fn local_storage_layout_uses_full_compile_with_build_info() {
576        let prj = test_project("cast-storage-build-info");
577        let target_path = prj.add_source("Target", "contract Target { uint256 value; }");
578        let mut config = Config::with_root(prj.root());
579        config.build_info = true;
580        let project = load_project_with_config(&prj, config);
581        let (_, address_code) = compile_target(&project, &target_path, "Target");
582
583        let artifact =
584            compile_local_storage_layout(&project, &address_code, true).unwrap().unwrap();
585        assert!(artifact.storage_layout.is_some());
586    }
587
588    #[test]
589    fn local_storage_layout_uses_full_compile_without_cache() {
590        let prj = test_project("cast-storage-no-cache");
591        let target_path = prj.add_source("Target", "contract Target { uint256 value; }");
592        let project = load_project(&prj);
593        let mut code_project = project.clone();
594        code_project.no_artifacts = true;
595        let (_, address_code) = compile_target(&code_project, &target_path, "Target");
596        assert!(!project.cache_path().exists());
597
598        let artifact =
599            compile_local_storage_layout(&project, &address_code, true).unwrap().unwrap();
600        assert!(artifact.storage_layout.is_some());
601    }
602
603    #[test]
604    fn parse_storage_etherscan_api_key() {
605        let args =
606            StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
607        assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
608
609        unsafe {
610            std::env::set_var("ETHERSCAN_API_KEY", "FXY");
611        }
612        let config = args.load_config().unwrap();
613        unsafe {
614            std::env::remove_var("ETHERSCAN_API_KEY");
615        }
616        assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
617        assert_eq!(config.get_etherscan_api_key(None).unwrap(), "dummykey".to_string());
618    }
619}