Skip to main content

cast/cmd/
storage.rs

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
37/// The minimum Solc version for outputting storage layouts.
38///
39/// <https://github.com/ethereum/solidity/blob/develop/Changelog.md#065-2020-04-06>
40const MIN_SOLC: Version = Version::new(0, 6, 5);
41
42/// CLI arguments for `cast storage`.
43#[derive(Clone, Debug, Parser)]
44pub struct StorageArgs {
45    /// The contract address.
46    #[arg(value_parser = NameOrAddress::from_str)]
47    address: NameOrAddress,
48
49    /// The storage slot number. If not provided, it gets the full storage layout.
50    #[arg(value_parser = parse_slot)]
51    base_slot: Option<B256>,
52
53    /// The storage offset from the base slot. If not provided, it is assumed to be zero.
54    #[arg(value_parser = str::parse::<U256>, default_value_t = U256::ZERO)]
55    offset: U256,
56
57    /// The known proxy address. If provided, the storage layout is retrieved from this address.
58    #[arg(long,value_parser = NameOrAddress::from_str)]
59    proxy: Option<NameOrAddress>,
60
61    /// The block height to query at.
62    ///
63    /// Can also be the tags earliest, finalized, safe, latest, or pending.
64    #[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    /// Specify the solc version to compile with. Overrides detected version.
77    #[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        // Slot was provided, perform a simple RPC call
106        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        // No slot was provided
121        // Get deployed bytecode at given address
122        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        // Check if we're in a forge project and if we can find the address' code
129        let mut project = build.project()?;
130        if project.paths.has_input_files() {
131            // Find in artifacts and pretty print
132            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        // Create or reuse a persistent cache for Etherscan sources; fall back to a temp dir
166        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        // Decide on compiler to use (user override -> metadata -> autodetect)
191        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        // Compile
210        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                // try recompiling with the minimum version
219                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/// Represents the value of a storage slot `eth_getStorageAt` call.
244#[derive(Clone, Debug, PartialEq, Eq)]
245struct StorageValue {
246    /// The slot number.
247    slot: B256,
248    /// The value as returned by `eth_getStorageAt`.
249    raw_slot_value: B256,
250}
251
252impl StorageValue {
253    /// Returns the value of the storage slot, applying the offset if necessary.
254    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        // reverse range, because the value is stored in big endian
265        let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
266
267        // copy the raw sliced value as tail
268        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/// Represents the storage layout of a contract and its values.
275#[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}