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};
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 slot: Option<B256>,
53
54 #[arg(long,value_parser = NameOrAddress::from_str)]
56 proxy: Option<NameOrAddress>,
57
58 #[arg(long, short)]
62 block: Option<BlockId>,
63
64 #[command(flatten)]
65 rpc: RpcOpts,
66
67 #[command(flatten)]
68 etherscan: EtherscanOpts,
69
70 #[command(flatten)]
71 build: BuildOpts,
72}
73
74impl_figment_convert_cast!(StorageArgs);
75
76impl figment::Provider for StorageArgs {
77 fn metadata(&self) -> Metadata {
78 Metadata::named("StorageArgs")
79 }
80
81 fn data(&self) -> Result<figment::value::Map<Profile, Dict>, figment::Error> {
82 let mut map = self.build.data()?;
83 let dict = map.get_mut(&Config::selected_profile()).unwrap();
84 dict.extend(self.rpc.dict());
85 dict.extend(self.etherscan.dict());
86 Ok(map)
87 }
88}
89
90impl StorageArgs {
91 pub async fn run(self) -> Result<()> {
92 let config = self.load_config()?;
93
94 let Self { address, slot, block, build, .. } = self;
95 let provider = utils::get_provider(&config)?;
96 let address = address.resolve(&provider).await?;
97
98 if let Some(slot) = slot {
100 let cast = Cast::new(provider);
101 sh_println!("{}", cast.storage(address, slot, block).await?)?;
102 return Ok(());
103 }
104
105 let address_code =
108 provider.get_code_at(address).block_id(block.unwrap_or_default()).await?;
109 if address_code.is_empty() {
110 eyre::bail!("Provided address has no deployed code and thus no storage");
111 }
112
113 let mut project = build.project()?;
115 if project.paths.has_input_files() {
116 add_storage_layout_output(&mut project);
118 let out = ProjectCompiler::new().quiet(shell::is_json()).compile(&project)?;
119 let artifact = out.artifacts().find(|(_, artifact)| {
120 artifact.get_deployed_bytecode_bytes().is_some_and(|b| *b == address_code)
121 });
122 if let Some((_, artifact)) = artifact {
123 return fetch_and_print_storage(
124 provider,
125 address,
126 block,
127 artifact,
128 !shell::is_json(),
129 )
130 .await;
131 }
132 }
133
134 if !self.etherscan.has_key() {
135 eyre::bail!(
136 "You must provide an Etherscan API key if you're fetching a remote contract's storage."
137 );
138 }
139
140 let chain = utils::get_chain(config.chain, &provider).await?;
141 let api_version = config.get_etherscan_api_version(Some(chain));
142 let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
143 let client = Client::new_with_api_version(chain, api_key, api_version)?;
144 let source = if let Some(proxy) = self.proxy {
145 find_source(client, proxy.resolve(&provider).await?).await?
146 } else {
147 find_source(client, address).await?
148 };
149 let metadata = source.items.first().unwrap();
150 if metadata.is_vyper() {
151 eyre::bail!("Contract at provided address is not a valid Solidity contract")
152 }
153
154 let version = metadata.compiler_version()?;
155 let auto_detect = version < MIN_SOLC;
156
157 let root = tempfile::tempdir()?;
160 let root_path = root.path();
161 let mut project = etherscan_project(metadata, root_path)?;
162 add_storage_layout_output(&mut project);
163
164 project.compiler = if auto_detect {
165 SolcCompiler::AutoDetect
166 } else {
167 SolcCompiler::Specific(Solc::find_or_install(&version)?)
168 };
169
170 let mut out = ProjectCompiler::new().quiet(true).compile(&project)?;
172 let artifact = {
173 let (_, mut artifact) = out
174 .artifacts()
175 .find(|(name, _)| name == &metadata.contract_name)
176 .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
177
178 if is_storage_layout_empty(&artifact.storage_layout) && auto_detect {
179 sh_warn!(
181 "The requested contract was compiled with {version} while the minimum version for storage layouts is {MIN_SOLC} and as a result the output may be empty."
182 )?;
183 let solc = Solc::find_or_install(&MIN_SOLC)?;
184 project.compiler = SolcCompiler::Specific(solc);
185 if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) {
186 out = output;
187 let (_, new_artifact) = out
188 .artifacts()
189 .find(|(name, _)| name == &metadata.contract_name)
190 .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
191 artifact = new_artifact;
192 }
193 }
194
195 artifact
196 };
197
198 root.close()?;
200
201 fetch_and_print_storage(provider, address, block, artifact, !shell::is_json()).await
202 }
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
207struct StorageValue {
208 slot: B256,
210 raw_slot_value: B256,
212}
213
214impl StorageValue {
215 fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
217 let offset = offset as usize;
218 let mut end = 32;
219 if let Some(number_of_bytes) = number_of_bytes {
220 end = offset + number_of_bytes;
221 if end > 32 {
222 end = 32;
223 }
224 }
225
226 let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
228
229 let mut value = [0u8; 32];
231 value[32 - raw_sliced_value.len()..32].copy_from_slice(raw_sliced_value);
232 B256::from(value)
233 }
234}
235
236#[derive(Clone, Debug, Serialize, Deserialize)]
238struct StorageReport {
239 #[serde(flatten)]
240 layout: StorageLayout,
241 values: Vec<B256>,
242}
243
244async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
245 provider: P,
246 address: Address,
247 block: Option<BlockId>,
248 artifact: &ConfigurableContractArtifact,
249 pretty: bool,
250) -> Result<()> {
251 if is_storage_layout_empty(&artifact.storage_layout) {
252 sh_warn!("Storage layout is empty.")?;
253 Ok(())
254 } else {
255 let layout = artifact.storage_layout.as_ref().unwrap().clone();
256 let values = fetch_storage_slots(provider, address, block, &layout).await?;
257 print_storage(layout, values, pretty)
258 }
259}
260
261async fn fetch_storage_slots<P: Provider<AnyNetwork>>(
262 provider: P,
263 address: Address,
264 block: Option<BlockId>,
265 layout: &StorageLayout,
266) -> Result<Vec<StorageValue>> {
267 let requests = layout.storage.iter().map(|storage_slot| async {
268 let slot = B256::from(U256::from_str(&storage_slot.slot)?);
269 let raw_slot_value = provider
270 .get_storage_at(address, slot.into())
271 .block_id(block.unwrap_or_default())
272 .await?;
273
274 let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() };
275
276 Ok(value)
277 });
278
279 futures::future::try_join_all(requests).await
280}
281
282fn print_storage(layout: StorageLayout, values: Vec<StorageValue>, pretty: bool) -> Result<()> {
283 if !pretty {
284 let values: Vec<_> = layout
285 .storage
286 .iter()
287 .zip(&values)
288 .map(|(slot, storage_value)| {
289 let storage_type = layout.types.get(&slot.storage_type);
290 storage_value.value(
291 slot.offset,
292 storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
293 )
294 })
295 .collect();
296 sh_println!(
297 "{}",
298 serde_json::to_string_pretty(&serde_json::to_value(StorageReport { layout, values })?)?
299 )?;
300 return Ok(());
301 }
302
303 let mut table = Table::new();
304 table.apply_modifier(UTF8_ROUND_CORNERS);
305
306 table.set_header(vec![
307 Cell::new("Name"),
308 Cell::new("Type"),
309 Cell::new("Slot"),
310 Cell::new("Offset"),
311 Cell::new("Bytes"),
312 Cell::new("Value"),
313 Cell::new("Hex Value"),
314 Cell::new("Contract"),
315 ]);
316
317 for (slot, storage_value) in layout.storage.into_iter().zip(values) {
318 let storage_type = layout.types.get(&slot.storage_type);
319 let value = storage_value
320 .value(slot.offset, storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()));
321 let converted_value = U256::from_be_bytes(value.0);
322
323 table.add_row([
324 slot.label.as_str(),
325 storage_type.map_or("?", |t| &t.label),
326 &slot.slot,
327 &slot.offset.to_string(),
328 storage_type.map_or("?", |t| &t.number_of_bytes),
329 &converted_value.to_string(),
330 &value.to_string(),
331 &slot.contract,
332 ]);
333 }
334
335 sh_println!("\n{table}\n")?;
336
337 Ok(())
338}
339
340fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(project: &mut Project<C>) {
341 project.artifacts.additional_values.storage_layout = true;
342 project.update_output_selection(|selection| {
343 selection.0.values_mut().for_each(|contract_selection| {
344 contract_selection
345 .values_mut()
346 .for_each(|selection| selection.push("storageLayout".to_string()))
347 });
348 })
349}
350
351fn is_storage_layout_empty(storage_layout: &Option<StorageLayout>) -> bool {
352 if let Some(s) = storage_layout { s.storage.is_empty() } else { true }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn parse_storage_etherscan_api_key() {
361 let args =
362 StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
363 assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
364
365 unsafe {
366 std::env::set_var("ETHERSCAN_API_KEY", "FXY");
367 }
368 let config = args.load_config().unwrap();
369 unsafe {
370 std::env::remove_var("ETHERSCAN_API_KEY");
371 }
372 assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
373
374 let key = config.get_etherscan_api_key(None).unwrap();
375 assert_eq!(key, "dummykey".to_string());
376 }
377}