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_block_explorers::Client;
11use foundry_cli::{
12 opts::{BuildOpts, EtherscanOpts, RpcOpts},
13 utils,
14 utils::LoadConfig,
15};
16use foundry_common::{
17 abi::find_source,
18 compile::{ProjectCompiler, etherscan_project},
19 shell,
20};
21use foundry_compilers::{
22 Artifact, Project,
23 artifacts::{ConfigurableContractArtifact, Contract, StorageLayout},
24 compilers::{
25 Compiler,
26 solc::{Solc, SolcCompiler},
27 },
28};
29use foundry_config::{
30 Config,
31 figment::{self, Metadata, Profile, value::Dict},
32 impl_figment_convert_cast,
33};
34use semver::Version;
35use serde::{Deserialize, Serialize};
36use std::str::FromStr;
37
38const MIN_SOLC: Version = Version::new(0, 6, 5);
42
43#[derive(Clone, Debug, Parser)]
45pub struct StorageArgs {
46 #[arg(value_parser = NameOrAddress::from_str)]
48 address: NameOrAddress,
49
50 #[arg(value_parser = parse_slot)]
52 base_slot: Option<B256>,
53
54 #[arg(value_parser = str::parse::<U256>, default_value_t = U256::ZERO)]
56 offset: U256,
57
58 #[arg(long,value_parser = NameOrAddress::from_str)]
60 proxy: Option<NameOrAddress>,
61
62 #[arg(long, short)]
66 block: Option<BlockId>,
67
68 #[command(flatten)]
69 rpc: RpcOpts,
70
71 #[command(flatten)]
72 etherscan: EtherscanOpts,
73
74 #[command(flatten)]
75 build: BuildOpts,
76
77 #[arg(long, value_parser = Version::parse)]
79 solc_version: Option<Version>,
80}
81
82impl_figment_convert_cast!(StorageArgs);
83
84impl figment::Provider for StorageArgs {
85 fn metadata(&self) -> Metadata {
86 Metadata::named("StorageArgs")
87 }
88
89 fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
90 let mut map = self.build.data()?;
91 let dict = map.get_mut(&Config::selected_profile()).unwrap();
92 dict.extend(self.rpc.dict());
93 dict.extend(self.etherscan.dict());
94 Ok(map)
95 }
96}
97
98impl StorageArgs {
99 pub async fn run(self) -> Result<()> {
100 let config = self.load_config()?;
101
102 let Self { address, base_slot, offset, block, build, .. } = self;
103 let provider = utils::get_provider(&config)?;
104 let address = address.resolve(&provider).await?;
105
106 if let Some(slot) = base_slot {
108 let cast = Cast::new(provider);
109 sh_println!(
110 "{}",
111 cast.storage(
112 address,
113 (Into::<U256>::into(slot).saturating_add(offset)).into(),
114 block
115 )
116 .await?
117 )?;
118 return Ok(());
119 }
120
121 let address_code =
124 provider.get_code_at(address).block_id(block.unwrap_or_default()).await?;
125 if address_code.is_empty() {
126 eyre::bail!("Provided address has no deployed code and thus no storage");
127 }
128
129 let mut project = build.project()?;
131 if project.paths.has_input_files() {
132 add_storage_layout_output(&mut project);
134 let out = ProjectCompiler::new().quiet(shell::is_json()).compile(&project)?;
135 let artifact = out.artifacts().find(|(_, artifact)| {
136 artifact.get_deployed_bytecode_bytes().is_some_and(|b| *b == address_code)
137 });
138 if let Some((_, artifact)) = artifact {
139 return fetch_and_print_storage(provider, address, block, artifact).await;
140 }
141 }
142
143 let chain = utils::get_chain(config.chain, &provider).await?;
144 let api_key = config.get_etherscan_api_key(Some(chain)).or_else(|| self.etherscan.key()).ok_or_else(|| {
145 eyre::eyre!("You must provide an Etherscan API key if you're fetching a remote contract's storage.")
146 })?;
147 let client = Client::new(chain, api_key)?;
148 let source = if let Some(proxy) = self.proxy {
149 find_source(client, proxy.resolve(&provider).await?).await?
150 } else {
151 find_source(client, address).await?
152 };
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 root_path = if let Some(cache_root) =
160 foundry_config::Config::foundry_etherscan_chain_cache_dir(chain)
161 {
162 let sources_root = cache_root.join("sources");
163 let contract_root = sources_root.join(format!("{address}"));
164 if let Err(err) = std::fs::create_dir_all(&contract_root) {
165 sh_warn!("Could not create etherscan cache dir, falling back to temp: {err}")?;
166 tempfile::tempdir()?.path().to_path_buf()
167 } else {
168 contract_root
169 }
170 } else {
171 tempfile::tempdir()?.keep()
172 };
173 let mut project = etherscan_project(metadata, &root_path)?;
174 add_storage_layout_output(&mut project);
175
176 let meta_version = metadata.compiler_version()?;
178 let mut auto_detect = false;
179 let desired = if let Some(user_version) = self.solc_version {
180 if user_version < MIN_SOLC {
181 sh_warn!(
182 "The provided --solc-version is {user_version} while the minimum version for \
183 storage layouts is {MIN_SOLC} and as a result the output may be empty."
184 )?;
185 }
186 SolcCompiler::Specific(Solc::find_or_install(&user_version)?)
187 } else if meta_version < MIN_SOLC {
188 auto_detect = true;
189 SolcCompiler::AutoDetect
190 } else {
191 SolcCompiler::Specific(Solc::find_or_install(&meta_version)?)
192 };
193 project.compiler.solc = Some(desired);
194
195 let mut out = ProjectCompiler::new().quiet(true).compile(&project)?;
197 let artifact = {
198 let (_, mut artifact) = out
199 .artifacts()
200 .find(|(name, _)| name == &metadata.contract_name)
201 .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
202
203 if auto_detect && is_storage_layout_empty(&artifact.storage_layout) {
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 let solc = Solc::find_or_install(&MIN_SOLC)?;
210 project.compiler.solc = Some(SolcCompiler::Specific(solc));
211 if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) {
212 out = output;
213 let (_, new_artifact) = out
214 .artifacts()
215 .find(|(name, _)| name == &metadata.contract_name)
216 .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
217 artifact = new_artifact;
218 }
219 }
220
221 artifact
222 };
223
224 fetch_and_print_storage(provider, address, block, artifact).await
225 }
226}
227
228#[derive(Clone, Debug, PartialEq, Eq)]
230struct StorageValue {
231 slot: B256,
233 raw_slot_value: B256,
235}
236
237impl StorageValue {
238 fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
240 let offset = offset as usize;
241 let mut end = 32;
242 if let Some(number_of_bytes) = number_of_bytes {
243 end = offset + number_of_bytes;
244 if end > 32 {
245 end = 32;
246 }
247 }
248
249 let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
251
252 let mut value = [0u8; 32];
254 value[32 - raw_sliced_value.len()..32].copy_from_slice(raw_sliced_value);
255 B256::from(value)
256 }
257}
258
259#[derive(Clone, Debug, Serialize, Deserialize)]
261struct StorageReport {
262 #[serde(flatten)]
263 layout: StorageLayout,
264 values: Vec<B256>,
265}
266
267async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
268 provider: P,
269 address: Address,
270 block: Option<BlockId>,
271 artifact: &ConfigurableContractArtifact,
272) -> Result<()> {
273 if is_storage_layout_empty(&artifact.storage_layout) {
274 sh_warn!("Storage layout is empty.")?;
275 Ok(())
276 } else {
277 let layout = artifact.storage_layout.as_ref().unwrap().clone();
278 let values = fetch_storage_slots(provider, address, block, &layout).await?;
279 print_storage(layout, values)
280 }
281}
282
283async fn fetch_storage_slots<P: Provider<AnyNetwork>>(
284 provider: P,
285 address: Address,
286 block: Option<BlockId>,
287 layout: &StorageLayout,
288) -> Result<Vec<StorageValue>> {
289 let requests = layout.storage.iter().map(|storage_slot| async {
290 let slot = B256::from(U256::from_str(&storage_slot.slot)?);
291 let raw_slot_value = provider
292 .get_storage_at(address, slot.into())
293 .block_id(block.unwrap_or_default())
294 .await?;
295
296 let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() };
297
298 Ok(value)
299 });
300
301 futures::future::try_join_all(requests).await
302}
303
304fn print_storage(layout: StorageLayout, values: Vec<StorageValue>) -> Result<()> {
305 if shell::is_json() {
306 let values: Vec<_> = layout
307 .storage
308 .iter()
309 .zip(&values)
310 .map(|(slot, storage_value)| {
311 let storage_type = layout.types.get(&slot.storage_type);
312 storage_value.value(
313 slot.offset,
314 storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
315 )
316 })
317 .collect();
318 sh_println!(
319 "{}",
320 serde_json::to_string_pretty(&serde_json::to_value(StorageReport { layout, values })?)?
321 )?;
322 return Ok(());
323 }
324
325 let mut table = Table::new();
326 if shell::is_markdown() {
327 table.load_preset(ASCII_MARKDOWN);
328 } else {
329 table.apply_modifier(UTF8_ROUND_CORNERS);
330 }
331
332 table.set_header(vec![
333 Cell::new("Name"),
334 Cell::new("Type"),
335 Cell::new("Slot"),
336 Cell::new("Offset"),
337 Cell::new("Bytes"),
338 Cell::new("Value"),
339 Cell::new("Hex Value"),
340 Cell::new("Contract"),
341 ]);
342
343 for (slot, storage_value) in layout.storage.into_iter().zip(values) {
344 let storage_type = layout.types.get(&slot.storage_type);
345 let value = storage_value
346 .value(slot.offset, storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()));
347 let converted_value = U256::from_be_bytes(value.0);
348
349 table.add_row([
350 slot.label.as_str(),
351 storage_type.map_or("?", |t| &t.label),
352 &slot.slot,
353 &slot.offset.to_string(),
354 storage_type.map_or("?", |t| &t.number_of_bytes),
355 &converted_value.to_string(),
356 &value.to_string(),
357 &slot.contract,
358 ]);
359 }
360
361 sh_println!("\n{table}\n")?;
362
363 Ok(())
364}
365
366fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(project: &mut Project<C>) {
367 project.artifacts.additional_values.storage_layout = true;
368 project.update_output_selection(|selection| {
369 selection.0.values_mut().for_each(|contract_selection| {
370 contract_selection
371 .values_mut()
372 .for_each(|selection| selection.push("storageLayout".to_string()))
373 });
374 })
375}
376
377fn is_storage_layout_empty(storage_layout: &Option<StorageLayout>) -> bool {
378 if let Some(s) = storage_layout { s.storage.is_empty() } else { true }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn parse_storage_etherscan_api_key() {
387 let args =
388 StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
389 assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
390
391 unsafe {
392 std::env::set_var("ETHERSCAN_API_KEY", "FXY");
393 }
394 let config = args.load_config().unwrap();
395 unsafe {
396 std::env::remove_var("ETHERSCAN_API_KEY");
397 }
398 assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
399
400 let key = config.get_etherscan_api_key(None).unwrap();
401 assert_eq!(key, "dummykey".to_string());
402 }
403
404 #[test]
405 fn parse_solc_version_arg() {
406 let args = StorageArgs::parse_from(["foundry-cli", "addr.eth", "--solc-version", "0.8.10"]);
407 assert_eq!(args.solc_version, Some(Version::parse("0.8.10").unwrap()));
408 }
409}