cast/cmd/
storage.rs

1use crate::{opts::parse_slot, Cast};
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::{modifiers::UTF8_ROUND_CORNERS, Cell, Table};
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::{etherscan_project, ProjectCompiler},
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
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    slot: Option<B256>,
52
53    /// The known proxy address. If provided, the storage layout is retrieved from this address.
54    #[arg(long,value_parser = NameOrAddress::from_str)]
55    proxy: Option<NameOrAddress>,
56
57    /// The block height to query at.
58    ///
59    /// Can also be the tags earliest, finalized, safe, latest, or pending.
60    #[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        // Slot was provided, perform a simple RPC call
98        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        // No slot was provided
105        // Get deployed bytecode at given address
106        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        // Check if we're in a forge project and if we can find the address' code
113        let mut project = build.project()?;
114        if project.paths.has_input_files() {
115            // Find in artifacts and pretty print
116            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_version = config.get_etherscan_api_version(Some(chain));
139        let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
140        let client = Client::new_with_api_version(chain, api_key, api_version)?;
141        let source = if let Some(proxy) = self.proxy {
142            find_source(client, proxy.resolve(&provider).await?).await?
143        } else {
144            find_source(client, address).await?
145        };
146        let metadata = source.items.first().unwrap();
147        if metadata.is_vyper() {
148            eyre::bail!("Contract at provided address is not a valid Solidity contract")
149        }
150
151        let version = metadata.compiler_version()?;
152        let auto_detect = version < MIN_SOLC;
153
154        // Create a new temp project
155        // TODO: Cache instead of using a temp directory: metadata from Etherscan won't change
156        let root = tempfile::tempdir()?;
157        let root_path = root.path();
158        let mut project = etherscan_project(metadata, root_path)?;
159        add_storage_layout_output(&mut project);
160
161        project.compiler = if auto_detect {
162            SolcCompiler::AutoDetect
163        } else {
164            SolcCompiler::Specific(Solc::find_or_install(&version)?)
165        };
166
167        // Compile
168        let mut out = ProjectCompiler::new().quiet(true).compile(&project)?;
169        let artifact = {
170            let (_, mut artifact) = out
171                .artifacts()
172                .find(|(name, _)| name == &metadata.contract_name)
173                .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
174
175            if is_storage_layout_empty(&artifact.storage_layout) && auto_detect {
176                // try recompiling with the minimum version
177                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.")?;
178                let solc = Solc::find_or_install(&MIN_SOLC)?;
179                project.compiler = SolcCompiler::Specific(solc);
180                if let Ok(output) = ProjectCompiler::new().quiet(true).compile(&project) {
181                    out = output;
182                    let (_, new_artifact) = out
183                        .artifacts()
184                        .find(|(name, _)| name == &metadata.contract_name)
185                        .ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
186                    artifact = new_artifact;
187                }
188            }
189
190            artifact
191        };
192
193        // Clear temp directory
194        root.close()?;
195
196        fetch_and_print_storage(provider, address, block, artifact, !shell::is_json()).await
197    }
198}
199
200/// Represents the value of a storage slot `eth_getStorageAt` call.
201#[derive(Clone, Debug, PartialEq, Eq)]
202struct StorageValue {
203    /// The slot number.
204    slot: B256,
205    /// The value as returned by `eth_getStorageAt`.
206    raw_slot_value: B256,
207}
208
209impl StorageValue {
210    /// Returns the value of the storage slot, applying the offset if necessary.
211    fn value(&self, offset: i64, number_of_bytes: Option<usize>) -> B256 {
212        let offset = offset as usize;
213        let mut end = 32;
214        if let Some(number_of_bytes) = number_of_bytes {
215            end = offset + number_of_bytes;
216            if end > 32 {
217                end = 32;
218            }
219        }
220
221        // reverse range, because the value is stored in big endian
222        let raw_sliced_value = &self.raw_slot_value.as_slice()[32 - end..32 - offset];
223
224        // copy the raw sliced value as tail
225        let mut value = [0u8; 32];
226        value[32 - raw_sliced_value.len()..32].copy_from_slice(raw_sliced_value);
227        B256::from(value)
228    }
229}
230
231/// Represents the storage layout of a contract and its values.
232#[derive(Clone, Debug, Serialize, Deserialize)]
233struct StorageReport {
234    #[serde(flatten)]
235    layout: StorageLayout,
236    values: Vec<B256>,
237}
238
239async fn fetch_and_print_storage<P: Provider<AnyNetwork>>(
240    provider: P,
241    address: Address,
242    block: Option<BlockId>,
243    artifact: &ConfigurableContractArtifact,
244    pretty: bool,
245) -> Result<()> {
246    if is_storage_layout_empty(&artifact.storage_layout) {
247        sh_warn!("Storage layout is empty.")?;
248        Ok(())
249    } else {
250        let layout = artifact.storage_layout.as_ref().unwrap().clone();
251        let values = fetch_storage_slots(provider, address, block, &layout).await?;
252        print_storage(layout, values, pretty)
253    }
254}
255
256async fn fetch_storage_slots<P: Provider<AnyNetwork>>(
257    provider: P,
258    address: Address,
259    block: Option<BlockId>,
260    layout: &StorageLayout,
261) -> Result<Vec<StorageValue>> {
262    let requests = layout.storage.iter().map(|storage_slot| async {
263        let slot = B256::from(U256::from_str(&storage_slot.slot)?);
264        let raw_slot_value = provider
265            .get_storage_at(address, slot.into())
266            .block_id(block.unwrap_or_default())
267            .await?;
268
269        let value = StorageValue { slot, raw_slot_value: raw_slot_value.into() };
270
271        Ok(value)
272    });
273
274    futures::future::try_join_all(requests).await
275}
276
277fn print_storage(layout: StorageLayout, values: Vec<StorageValue>, pretty: bool) -> Result<()> {
278    if !pretty {
279        let values: Vec<_> = layout
280            .storage
281            .iter()
282            .zip(&values)
283            .map(|(slot, storage_value)| {
284                let storage_type = layout.types.get(&slot.storage_type);
285                storage_value.value(
286                    slot.offset,
287                    storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()),
288                )
289            })
290            .collect();
291        sh_println!(
292            "{}",
293            serde_json::to_string_pretty(&serde_json::to_value(StorageReport { layout, values })?)?
294        )?;
295        return Ok(());
296    }
297
298    let mut table = Table::new();
299    table.apply_modifier(UTF8_ROUND_CORNERS);
300
301    table.set_header(vec![
302        Cell::new("Name"),
303        Cell::new("Type"),
304        Cell::new("Slot"),
305        Cell::new("Offset"),
306        Cell::new("Bytes"),
307        Cell::new("Value"),
308        Cell::new("Hex Value"),
309        Cell::new("Contract"),
310    ]);
311
312    for (slot, storage_value) in layout.storage.into_iter().zip(values) {
313        let storage_type = layout.types.get(&slot.storage_type);
314        let value = storage_value
315            .value(slot.offset, storage_type.and_then(|t| t.number_of_bytes.parse::<usize>().ok()));
316        let converted_value = U256::from_be_bytes(value.0);
317
318        table.add_row([
319            slot.label.as_str(),
320            storage_type.map_or("?", |t| &t.label),
321            &slot.slot,
322            &slot.offset.to_string(),
323            storage_type.map_or("?", |t| &t.number_of_bytes),
324            &converted_value.to_string(),
325            &value.to_string(),
326            &slot.contract,
327        ]);
328    }
329
330    sh_println!("\n{table}\n")?;
331
332    Ok(())
333}
334
335fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(project: &mut Project<C>) {
336    project.artifacts.additional_values.storage_layout = true;
337    project.update_output_selection(|selection| {
338        selection.0.values_mut().for_each(|contract_selection| {
339            contract_selection
340                .values_mut()
341                .for_each(|selection| selection.push("storageLayout".to_string()))
342        });
343    })
344}
345
346fn is_storage_layout_empty(storage_layout: &Option<StorageLayout>) -> bool {
347    if let Some(ref s) = storage_layout {
348        s.storage.is_empty()
349    } else {
350        true
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn parse_storage_etherscan_api_key() {
360        let args =
361            StorageArgs::parse_from(["foundry-cli", "addr.eth", "--etherscan-api-key", "dummykey"]);
362        assert_eq!(args.etherscan.key(), Some("dummykey".to_string()));
363
364        std::env::set_var("ETHERSCAN_API_KEY", "FXY");
365        let config = args.load_config().unwrap();
366        std::env::remove_var("ETHERSCAN_API_KEY");
367        assert_eq!(config.etherscan_api_key, Some("dummykey".to_string()));
368
369        let key = config.get_etherscan_api_key(None).unwrap();
370        assert_eq!(key, "dummykey".to_string());
371    }
372}