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
37const MIN_SOLC: Version = Version::new(0, 6, 5);
41
42#[derive(Clone, Debug, Parser)]
44pub struct StorageArgs {
45 #[arg(value_parser = NameOrAddress::from_str)]
47 address: NameOrAddress,
48
49 #[arg(value_parser = parse_slot)]
51 base_slot: Option<B256>,
52
53 #[arg(value_parser = str::parse::<U256>, default_value_t = U256::ZERO)]
55 offset: U256,
56
57 #[arg(long,value_parser = NameOrAddress::from_str)]
59 proxy: Option<NameOrAddress>,
60
61 #[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 #[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 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 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 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 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 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 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
219fn compile_local_storage_layout(
224 project: &Project,
225 address_code: &Bytes,
226 json: bool,
227) -> Result<Option<ConfigurableContractArtifact>> {
228 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
290fn 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#[derive(Clone, Debug, PartialEq, Eq)]
304struct StorageValue {
305 slot: B256,
307 raw_slot_value: B256,
309}
310
311impl StorageValue {
312 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 B256::left_padding_from(&self.raw_slot_value[32 - end..32 - offset])
318 }
319}
320
321#[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 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}