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
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 = 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 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 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 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 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 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 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
229fn compile_local_storage_layout(
234 project: &Project,
235 address_code: &Bytes,
236 json: bool,
237) -> Result<Option<ConfigurableContractArtifact>> {
238 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
263fn 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
305fn 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
345fn 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#[derive(Clone, Debug, PartialEq, Eq)]
359struct StorageValue {
360 slot: B256,
362 raw_slot_value: B256,
364}
365
366impl StorageValue {
367 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 B256::left_padding_from(&self.raw_slot_value[32 - end..32 - offset])
373 }
374}
375
376#[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 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 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 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}