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