1use crate::{Cast, opts::parse_slot};
2use alloy_ens::NameOrAddress;
3use alloy_network::AnyNetwork;
4use alloy_primitives::{Address, B256, U256};
5use alloy_provider::Provider;
6use alloy_rpc_types::BlockId;
7use clap::Parser;
8use comfy_table::{Cell, Table, modifiers::UTF8_ROUND_CORNERS, presets::ASCII_MARKDOWN};
9use eyre::Result;
10use foundry_cli::{
11 opts::{BuildOpts, EtherscanOpts, RpcOpts},
12 utils,
13 utils::LoadConfig,
14};
15use foundry_common::{
16 abi::find_source,
17 compile::{ProjectCompiler, etherscan_project},
18 shell,
19};
20use foundry_compilers::{
21 Artifact, Project,
22 artifacts::{ConfigurableContractArtifact, Contract, StorageLayout},
23 compilers::{
24 Compiler,
25 solc::{Solc, SolcCompiler},
26 },
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 cast = Cast::new(provider);
108 sh_println!(
109 "{}",
110 cast.storage(
111 address,
112 (Into::<U256>::into(slot).saturating_add(offset)).into(),
113 block
114 )
115 .await?
116 )?;
117 return Ok(());
118 }
119
120 let address_code =
123 provider.get_code_at(address).block_id(block.unwrap_or_default()).await?;
124 if address_code.is_empty() {
125 eyre::bail!("Provided address has no deployed code and thus no storage");
126 }
127
128 let mut project = build.project()?;
130 if project.paths.has_input_files() {
131 add_storage_layout_output(&mut project);
133 let out = ProjectCompiler::new().quiet(shell::is_json()).compile(&project)?;
134 let artifact = out.artifacts().find(|(_, artifact)| {
135 artifact.get_deployed_bytecode_bytes().is_some_and(|b| *b == address_code)
136 });
137 if let Some((_, artifact)) = artifact {
138 return fetch_and_print_storage(provider, address, block, artifact).await;
139 }
140 }
141
142 let chain = utils::get_chain(config.chain, &provider).await?;
143 let etherscan_api_key = self.etherscan.key();
144 let client = match config.get_etherscan_config_with_chain(Some(chain))? {
145 Some(etherscan_config) => {
146 etherscan_config.into_client_with_no_proxy(config.eth_rpc_no_proxy)?
147 }
148 None => {
149 let api_key = etherscan_api_key.ok_or_else(|| {
150 eyre::eyre!("You must provide an Etherscan API key if you're fetching a remote contract's storage.")
151 })?;
152 foundry_block_explorers::Client::new(chain, api_key)?
153 }
154 };
155 let source = if let Some(proxy) = self.proxy {
156 find_source(client, proxy.resolve(&provider).await?).await?
157 } else {
158 find_source(client, address).await?
159 };
160 let metadata = source.items.first().unwrap();
161 if metadata.is_vyper() {
162 eyre::bail!("Contract at provided address is not a valid Solidity contract")
163 }
164
165 let mut temp_dir = None;
167 let root_path = if let Some(cache_root) =
168 foundry_config::Config::foundry_etherscan_chain_cache_dir(chain)
169 {
170 let sources_root = cache_root.join("sources");
171 let contract_root = sources_root.join(format!("{address}"));
172 if let Err(err) = std::fs::create_dir_all(&contract_root) {
173 sh_warn!("Could not create etherscan cache dir, falling back to temp: {err}")?;
174 let tmp = tempfile::tempdir()?;
175 let path = tmp.path().to_path_buf();
176 temp_dir = Some(tmp);
177 path
178 } else {
179 contract_root
180 }
181 } else {
182 let tmp = tempfile::tempdir()?;
183 let path = tmp.path().to_path_buf();
184 temp_dir = Some(tmp);
185 path
186 };
187 let mut project = etherscan_project(metadata, &root_path)?;
188 add_storage_layout_output(&mut project);
189
190 let meta_version = metadata.compiler_version()?;
192 let mut auto_detect = false;
193 let desired = if let Some(user_version) = self.solc_version {
194 if user_version < MIN_SOLC {
195 sh_warn!(
196 "The provided --solc-version is {user_version} while the minimum version for \
197 storage layouts is {MIN_SOLC} and as a result the output may be empty."
198 )?;
199 }
200 SolcCompiler::Specific(Solc::find_or_install(&user_version)?)
201 } else if meta_version < MIN_SOLC {
202 auto_detect = true;
203 SolcCompiler::AutoDetect
204 } else {
205 SolcCompiler::Specific(Solc::find_or_install(&meta_version)?)
206 };
207 project.compiler.solc = Some(desired);
208
209 let mut out = ProjectCompiler::new().quiet(true).compile(&project)?;
211 let artifact = {
212 let (_, mut artifact) = out
213 .artifacts()
214 .find(|(name, _)| name == &metadata.contract_name)
215 .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
216
217 if auto_detect && is_storage_layout_empty(&artifact.storage_layout) {
218 sh_warn!(
220 "The requested contract was compiled with {meta_version} while the minimum version \
221 for storage layouts is {MIN_SOLC} and as a result the output may be empty.",
222 )?;
223 let solc = Solc::find_or_install(&MIN_SOLC)?;
224 project.compiler.solc = Some(SolcCompiler::Specific(solc));
225 if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) {
226 out = output;
227 let (_, new_artifact) = out
228 .artifacts()
229 .find(|(name, _)| name == &metadata.contract_name)
230 .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
231 artifact = new_artifact;
232 }
233 }
234
235 artifact
236 };
237
238 drop(temp_dir);
239 fetch_and_print_storage(provider, address, block, artifact).await
240 }
241}
242
243#[derive(Clone, Debug, PartialEq, Eq)]
245struct StorageValue {
246 slot: B256,
248 raw_slot_value: B256,
250}
251
252impl StorageValue {
253 fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
255 let offset = offset as usize;
256 let mut end = 32;
257 if let Some(number_of_bytes) = number_of_bytes {
258 end = offset + number_of_bytes;
259 if end > 32 {
260 end = 32;
261 }
262 }
263
264 let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
266
267 let mut value = [0u8; 32];
269 value[32 - raw_sliced_value.len()..32].copy_from_slice(raw_sliced_value);
270 B256::from(value)
271 }
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize)]
276struct StorageReport {
277 #[serde(flatten)]
278 layout: StorageLayout,
279 values: Vec<B256>,
280}
281
282async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
283 provider: P,
284 address: Address,
285 block: Option<BlockId>,
286 artifact: &ConfigurableContractArtifact,
287) -> Result<()> {
288 if is_storage_layout_empty(&artifact.storage_layout) {
289 sh_warn!("Storage layout is empty.")?;
290 Ok(())
291 } else {
292 let layout = artifact.storage_layout.as_ref().unwrap().clone();
293 let values = fetch_storage_slots(provider, address, block, &layout).await?;
294 print_storage(layout, values)
295 }
296}
297
298async fn fetch_storage_slots<P: Provider<AnyNetwork>>(
299 provider: P,
300 address: Address,
301 block: Option<BlockId>,
302 layout: &StorageLayout,
303) -> Result<Vec<StorageValue>> {
304 let requests = layout.storage.iter().map(|storage_slot| async {
305 let slot = B256::from(U256::from_str(&storage_slot.slot)?);
306 let raw_slot_value = provider
307 .get_storage_at(address, slot.into())
308 .block_id(block.unwrap_or_default())
309 .await?;
310
311 let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() };
312
313 Ok(value)
314 });
315
316 futures::future::try_join_all(requests).await
317}
318
319fn print_storage(layout: StorageLayout, values: Vec<StorageValue>) -> Result<()> {
320 if shell::is_json() {
321 let values: Vec<_> = layout
322 .storage
323 .iter()
324 .zip(&values)
325 .map(|(slot, storage_value)| {
326 let storage_type = layout.types.get(&slot.storage_type);
327 storage_value.value(
328 slot.offset,
329 storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
330 )
331 })
332 .collect();
333 sh_println!(
334 "{}",
335 serde_json::to_string_pretty(&serde_json::to_value(StorageReport { layout, values })?)?
336 )?;
337 return Ok(());
338 }
339
340 let mut table = Table::new();
341 if shell::is_markdown() {
342 table.load_preset(ASCII_MARKDOWN);
343 } else {
344 table.apply_modifier(UTF8_ROUND_CORNERS);
345 }
346
347 table.set_header(vec![
348 Cell::new("Name"),
349 Cell::new("Type"),
350 Cell::new("Slot"),
351 Cell::new("Offset"),
352 Cell::new("Bytes"),
353 Cell::new("Value"),
354 Cell::new("Hex Value"),
355 Cell::new("Contract"),
356 ]);
357
358 for (slot, storage_value) in layout.storage.into_iter().zip(values) {
359 let storage_type = layout.types.get(&slot.storage_type);
360 let value = storage_value
361 .value(slot.offset, storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()));
362 let converted_value = U256::from_be_bytes(value.0);
363
364 table.add_row([
365 slot.label.as_str(),
366 storage_type.map_or("?", |t| &t.label),
367 &slot.slot,
368 &slot.offset.to_string(),
369 storage_type.map_or("?", |t| &t.number_of_bytes),
370 &converted_value.to_string(),
371 &value.to_string(),
372 &slot.contract,
373 ]);
374 }
375
376 sh_println!("\n{table}\n")?;
377
378 Ok(())
379}
380
381fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(project: &mut Project<C>) {
382 project.artifacts.additional_values.storage_layout = true;
383 project.update_output_selection(|selection| {
384 for contract_selection in selection.0.values_mut() {
385 for selection in contract_selection.values_mut() {
386 selection.push("storageLayout".to_string());
387 }
388 }
389 })
390}
391
392const fn is_storage_layout_empty(storage_layout: &Option<StorageLayout>) -> bool {
393 if let Some(s) = storage_layout { s.storage.is_empty() } else { true }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn parse_storage_etherscan_api_key() {
402 let args =
403 StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
404 assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
405
406 unsafe {
407 std::env::set_var("ETHERSCAN_API_KEY", "FXY");
408 }
409 let config = args.load_config().unwrap();
410 unsafe {
411 std::env::remove_var("ETHERSCAN_API_KEY");
412 }
413 assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
414
415 let key = config.get_etherscan_api_key(None).unwrap();
416 assert_eq!(key, "dummykey".to_string());
417 }
418
419 #[test]
420 fn parse_solc_version_arg() {
421 let args = StorageArgs::parse_from(["foundry-cli", "addr.eth", "--solc-version", "0.8.10"]);
422 assert_eq!(args.solc_version, Some(Version::parse("0.8.10").unwrap()));
423 }
424}