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_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
38/// The minimum Solc version for outputting storage layouts.
39///
40/// <https://github.com/ethereum/solidity/blob/develop/Changelog.md#065-2020-04-06>
41const MIN_SOLC: Version = Version::new(0, 6, 5);
42
43/// CLI arguments for `cast storage`.
44#[derive(Clone, Debug, Parser)]
45pub struct StorageArgs {
46    /// The contract address.
47    #[arg(value_parser = NameOrAddress::from_str)]
48    address: NameOrAddress,
49
50    /// The storage slot number. If not provided, it gets the full storage layout.
51    #[arg(value_parser = parse_slot)]
52    base_slot: Option<B256>,
53
54    /// The storage offset from the base slot. If not provided, it is assumed to be zero.
55    #[arg(value_parser = str::parse::<U256>, default_value_t = U256::ZERO)]
56    offset: U256,
57
58    /// The known proxy address. If provided, the storage layout is retrieved from this address.
59    #[arg(long,value_parser = NameOrAddress::from_str)]
60    proxy: Option<NameOrAddress>,
61
62    /// The block height to query at.
63    ///
64    /// Can also be the tags earliest, finalized, safe, latest, or pending.
65    #[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    /// Specify the solc version to compile with. Overrides detected version.
78    #[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        // Slot was provided, perform a simple RPC call
107        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        // No slot was provided
122        // Get deployed bytecode at given address
123        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        // Check if we're in a forge project and if we can find the address' code
130        let mut project = build.project()?;
131        if project.paths.has_input_files() {
132            // Find in artifacts and pretty print
133            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        // Create or reuse a persistent cache for Etherscan sources; fall back to a temp dir
159        let mut temp_dir = None;
160        let root_path = if let Some(cache_root) =
161            foundry_config::Config::foundry_etherscan_chain_cache_dir(chain)
162        {
163            let sources_root = cache_root.join("sources");
164            let contract_root = sources_root.join(format!("{address}"));
165            if let Err(err) = std::fs::create_dir_all(&contract_root) {
166                sh_warn!("Could not create etherscan cache dir, falling back to temp: {err}")?;
167                let tmp = tempfile::tempdir()?;
168                let path = tmp.path().to_path_buf();
169                temp_dir = Some(tmp);
170                path
171            } else {
172                contract_root
173            }
174        } else {
175            let tmp = tempfile::tempdir()?;
176            let path = tmp.path().to_path_buf();
177            temp_dir = Some(tmp);
178            path
179        };
180        let mut project = etherscan_project(metadata, &root_path)?;
181        add_storage_layout_output(&mut project);
182
183        // Decide on compiler to use (user override -> metadata -> autodetect)
184        let meta_version = metadata.compiler_version()?;
185        let mut auto_detect = false;
186        let desired = if let Some(user_version) = self.solc_version {
187            if user_version < MIN_SOLC {
188                sh_warn!(
189                    "The provided --solc-version is {user_version} while the minimum version for \
190                     storage layouts is {MIN_SOLC} and as a result the output may be empty."
191                )?;
192            }
193            SolcCompiler::Specific(Solc::find_or_install(&user_version)?)
194        } else if meta_version < MIN_SOLC {
195            auto_detect = true;
196            SolcCompiler::AutoDetect
197        } else {
198            SolcCompiler::Specific(Solc::find_or_install(&meta_version)?)
199        };
200        project.compiler.solc = Some(desired);
201
202        // Compile
203        let mut out = ProjectCompiler::new().quiet(true).compile(&project)?;
204        let artifact = {
205            let (_, mut artifact) = out
206                .artifacts()
207                .find(|(name, _)| name == &metadata.contract_name)
208                .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
209
210            if auto_detect && is_storage_layout_empty(&artifact.storage_layout) {
211                // try recompiling with the minimum version
212                sh_warn!(
213                    "The requested contract was compiled with {meta_version} while the minimum version \
214                     for storage layouts is {MIN_SOLC} and as a result the output may be empty.",
215                )?;
216                let solc = Solc::find_or_install(&MIN_SOLC)?;
217                project.compiler.solc = Some(SolcCompiler::Specific(solc));
218                if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) {
219                    out = output;
220                    let (_, new_artifact) = out
221                        .artifacts()
222                        .find(|(name, _)| name == &metadata.contract_name)
223                        .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
224                    artifact = new_artifact;
225                }
226            }
227
228            artifact
229        };
230
231        drop(temp_dir);
232        fetch_and_print_storage(provider, address, block, artifact).await
233    }
234}
235
236/// Represents the value of a storage slot `eth_getStorageAt` call.
237#[derive(Clone, Debug, PartialEq, Eq)]
238struct StorageValue {
239    /// The slot number.
240    slot: B256,
241    /// The value as returned by `eth_getStorageAt`.
242    raw_slot_value: B256,
243}
244
245impl StorageValue {
246    /// Returns the value of the storage slot, applying the offset if necessary.
247    fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
248        let offset = offset as usize;
249        let mut end = 32;
250        if let Some(number_of_bytes) = number_of_bytes {
251            end = offset + number_of_bytes;
252            if end > 32 {
253                end = 32;
254            }
255        }
256
257        // reverse range, because the value is stored in big endian
258        let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
259
260        // copy the raw sliced value as tail
261        let mut value = [0u8; 32];
262        value[32 - raw_sliced_value.len()..32].copy_from_slice(raw_sliced_value);
263        B256::from(value)
264    }
265}
266
267/// Represents the storage layout of a contract and its values.
268#[derive(Clone, Debug, Serialize, Deserialize)]
269struct StorageReport {
270    #[serde(flatten)]
271    layout: StorageLayout,
272    values: Vec<B256>,
273}
274
275async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
276    provider: P,
277    address: Address,
278    block: Option<BlockId>,
279    artifact: &ConfigurableContractArtifact,
280) -> Result<()> {
281    if is_storage_layout_empty(&artifact.storage_layout) {
282        sh_warn!("Storage layout is empty.")?;
283        Ok(())
284    } else {
285        let layout = artifact.storage_layout.as_ref().unwrap().clone();
286        let values = fetch_storage_slots(provider, address, block, &layout).await?;
287        print_storage(layout, values)
288    }
289}
290
291async fn fetch_storage_slots<P: Provider<AnyNetwork>>(
292    provider: P,
293    address: Address,
294    block: Option<BlockId>,
295    layout: &StorageLayout,
296) -> Result<Vec<StorageValue>> {
297    let requests = layout.storage.iter().map(|storage_slot| async {
298        let slot = B256::from(U256::from_str(&storage_slot.slot)?);
299        let raw_slot_value = provider
300            .get_storage_at(address, slot.into())
301            .block_id(block.unwrap_or_default())
302            .await?;
303
304        let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() };
305
306        Ok(value)
307    });
308
309    futures::future::try_join_all(requests).await
310}
311
312fn print_storage(layout: StorageLayout, values: Vec<StorageValue>) -> Result<()> {
313    if shell::is_json() {
314        let values: Vec<_> = layout
315            .storage
316            .iter()
317            .zip(&values)
318            .map(|(slot, storage_value)| {
319                let storage_type = layout.types.get(&slot.storage_type);
320                storage_value.value(
321                    slot.offset,
322                    storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
323                )
324            })
325            .collect();
326        sh_println!(
327            "{}",
328            serde_json::to_string_pretty(&serde_json::to_value(StorageReport { layout, values })?)?
329        )?;
330        return Ok(());
331    }
332
333    let mut table = Table::new();
334    if shell::is_markdown() {
335        table.load_preset(ASCII_MARKDOWN);
336    } else {
337        table.apply_modifier(UTF8_ROUND_CORNERS);
338    }
339
340    table.set_header(vec![
341        Cell::new("Name"),
342        Cell::new("Type"),
343        Cell::new("Slot"),
344        Cell::new("Offset"),
345        Cell::new("Bytes"),
346        Cell::new("Value"),
347        Cell::new("Hex Value"),
348        Cell::new("Contract"),
349    ]);
350
351    for (slot, storage_value) in layout.storage.into_iter().zip(values) {
352        let storage_type = layout.types.get(&slot.storage_type);
353        let value = storage_value
354            .value(slot.offset, storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()));
355        let converted_value = U256::from_be_bytes(value.0);
356
357        table.add_row([
358            slot.label.as_str(),
359            storage_type.map_or("?", |t| &t.label),
360            &slot.slot,
361            &slot.offset.to_string(),
362            storage_type.map_or("?", |t| &t.number_of_bytes),
363            &converted_value.to_string(),
364            &value.to_string(),
365            &slot.contract,
366        ]);
367    }
368
369    sh_println!("\n{table}\n")?;
370
371    Ok(())
372}
373
374fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(project: &mut Project<C>) {
375    project.artifacts.additional_values.storage_layout = true;
376    project.update_output_selection(|selection| {
377        selection.0.values_mut().for_each(|contract_selection| {
378            contract_selection
379                .values_mut()
380                .for_each(|selection| selection.push("storageLayout".to_string()))
381        });
382    })
383}
384
385fn is_storage_layout_empty(storage_layout: &Option<StorageLayout>) -> bool {
386    if let Some(s) = storage_layout { s.storage.is_empty() } else { true }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn parse_storage_etherscan_api_key() {
395        let args =
396            StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
397        assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
398
399        unsafe {
400            std::env::set_var("ETHERSCAN_API_KEY", "FXY");
401        }
402        let config = args.load_config().unwrap();
403        unsafe {
404            std::env::remove_var("ETHERSCAN_API_KEY");
405        }
406        assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
407
408        let key = config.get_etherscan_api_key(None).unwrap();
409        assert_eq!(key, "dummykey".to_string());
410    }
411
412    #[test]
413    fn parse_solc_version_arg() {
414        let args = StorageArgs::parse_from(["foundry-cli", "addr.eth", "--solc-version", "0.8.10"]);
415        assert_eq!(args.solc_version, Some(Version::parse("0.8.10").unwrap()));
416    }
417}