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