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
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        // Slot was provided, perform a simple RPC call
103        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        // No slot was provided
118        // Get deployed bytecode at given address
119        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        // Check if we're in a forge project and if we can find the address' code
126        let mut project = build.project()?;
127        if project.paths.has_input_files() {
128            // Find in artifacts and pretty print
129            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        // Create a new temp project
166        // TODO: Cache instead of using a temp directory: metadata from Etherscan won't change
167        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        // Compile
184        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                // try recompiling with the minimum version
193                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        // Clear temp directory
213        root.close()?;
214
215        fetch_and_print_storage(provider, address, block, artifact, !shell::is_json()).await
216    }
217}
218
219/// Represents the value of a storage slot `eth_getStorageAt` call.
220#[derive(Clone, Debug, PartialEq, Eq)]
221struct StorageValue {
222    /// The slot number.
223    slot: B256,
224    /// The value as returned by `eth_getStorageAt`.
225    raw_slot_value: B256,
226}
227
228impl StorageValue {
229    /// Returns the value of the storage slot, applying the offset if necessary.
230    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        // reverse range, because the value is stored in big endian
241        let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
242
243        // copy the raw sliced value as tail
244        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/// Represents the storage layout of a contract and its values.
251#[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}