Skip to main content

foundry_cheatcodes/
external_storage.rs

1//! Storage layouts for contracts that aren't part of the local project.
2//!
3//! [`foundry_common::external_storage`] turns a verified source into a storage layout and caches
4//! the result; [`ExternalIdentifier`] finds that source on a block explorer. This module is the
5//! seam between them, and owns the state that has to outlive a single lookup.
6
7use alloy_primitives::{
8    Address,
9    map::{AddressMap, HashMap},
10};
11use foundry_common::external_storage::{fetch_external_storage_layouts, lock_until};
12use foundry_compilers::artifacts::StorageLayout;
13use foundry_config::Chain;
14use foundry_evm_traces::identifier::{ExternalIdentifier, ExternalIdentifierConfig};
15use std::{
16    sync::{Arc, LazyLock, Mutex},
17    time::Instant,
18};
19
20/// Chain id to the identifier for that chain, or `None` if one couldn't be built.
21type Identifiers = HashMap<u64, Option<Arc<Mutex<ExternalIdentifier>>>>;
22
23/// The [`ExternalIdentifier`] in use for each chain, shared by every test in the process.
24///
25/// One identifier per chain rather than one per lookup: it carries the metadata it has already
26/// fetched and the budget for how long identification may go on for, and neither means anything
27/// unless it survives across calls.
28static IDENTIFIERS: LazyLock<Mutex<Identifiers>> = LazyLock::new(Default::default);
29
30/// Resolves the storage layouts of contracts outside the local project.
31///
32/// Returns the layouts it could resolve; anything absent stays undecoded. Costs nothing for
33/// addresses already resolved in this process or by a previous run.
34pub(crate) fn storage_layouts(
35    sources: &ExternalIdentifierConfig,
36    chain: Chain,
37    addresses: Vec<Address>,
38) -> AddressMap<(String, Arc<StorageLayout>)> {
39    fetch_external_storage_layouts(
40        chain,
41        addresses,
42        sources.storage_timeout(),
43        |unresolved, timeout| {
44            let deadline = Instant::now().checked_add(timeout).unwrap_or_else(Instant::now);
45            let Some(identifier) = identifier(sources, chain, deadline) else {
46                return Default::default();
47            };
48            let Some(mut identifier) = lock_until(&identifier, deadline) else {
49                return Default::default();
50            };
51            let remaining = deadline.saturating_duration_since(Instant::now());
52            foundry_common::block_on(identifier.get_metadata(unresolved, remaining))
53        },
54    )
55}
56
57/// The identifier for `chain`, building it on first use.
58///
59/// Warns once per chain when there is nothing to look contracts up with, since the alternative is
60/// silently decoding nothing for a run the user explicitly asked to decode.
61fn identifier(
62    sources: &ExternalIdentifierConfig,
63    chain: Chain,
64    deadline: Instant,
65) -> Option<Arc<Mutex<ExternalIdentifier>>> {
66    let mut identifiers = lock_until(&IDENTIFIERS, deadline)?;
67    identifiers
68        .entry(chain.id())
69        .or_insert_with(|| match sources.storage_identifier(chain) {
70            Some(identifier) => Some(Arc::new(Mutex::new(identifier))),
71            None => {
72                let _ = sh_warn!(
73                    "cannot decode external storage on chain {chain}: no matching block explorer \
74                     is configured"
75                );
76                None
77            }
78        })
79        .clone()
80}