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
40const MIN_SOLC: Version = Version::new(0, 6, 5);
44
45#[derive(Clone, Debug, Parser)]
47pub struct StorageArgs {
48 #[arg(value_parser = NameOrAddress::from_str)]
50 address: NameOrAddress,
51
52 #[arg(value_parser = parse_slot)]
54 base_slot: Option<B256>,
55
56 #[arg(value_parser = str::parse::<U256>, default_value_t = U256::ZERO)]
58 offset: U256,
59
60 #[arg(long,value_parser = NameOrAddress::from_str)]
62 proxy: Option<NameOrAddress>,
63
64 #[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 #[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 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 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 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 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 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 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 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
241fn compile_local_storage_layout(
246 project: &Project,
247 address_code: &Bytes,
248 json: bool,
249) -> Result<Option<ConfigurableContractArtifact>> {
250 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
317fn 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#[derive(Clone, Debug, PartialEq, Eq)]
331struct StorageValue {
332 slot: B256,
334 raw_slot_value: B256,
336}
337
338impl StorageValue {
339 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 let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
352
353 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#[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}