Skip to main content

foundry_common/
external_storage.rs

1//! Storage layouts for contracts that are not part of the local project.
2//!
3//! When a forked test touches a contract that isn't in the local artifacts, its storage slots can
4//! still be decoded by compiling the verified source a block explorer has for it.
5//!
6//! Finding that source is the caller's job — `foundry-evm-traces` already knows how to ask a block
7//! explorer for it. This module owns what happens next: compiling it for a storage layout, which
8//! costs a full `solc` invocation, and making sure that cost is paid once.
9//! `forge test` runs test contracts in parallel, so lookups are:
10//!
11//! - deduplicated process-wide by `LOOKUPS`, so concurrent tests touching the same contract compile
12//!   it once instead of racing each other;
13//! - persisted to disk once resolved, so later runs skip straight to the layout.
14//!
15//! Unverified responses are memoized only for this run so later runs can discover newly verified
16//! contracts.
17
18use crate::fs;
19use alloy_chains::Chain;
20use alloy_primitives::{Address, map::AddressMap};
21use foundry_block_explorers::contract::Metadata;
22use foundry_compilers::{
23    artifacts::{
24        CompilerOutput, SolcInput, SolcLanguage, Source, Sources, StorageLayout,
25        output_selection::OutputSelection,
26    },
27    solc::Solc,
28};
29use foundry_config::Config;
30use serde::{Deserialize, Serialize};
31use std::{
32    io::{Read, Seek, SeekFrom},
33    path::{Path, PathBuf},
34    process::{Command, Stdio},
35    sync::{
36        Arc, LazyLock, Mutex, MutexGuard, TryLockError,
37        atomic::{AtomicU64, Ordering},
38    },
39    time::{Duration, Instant},
40};
41use wait_timeout::ChildExt;
42
43/// First solc release that emits `storageLayout`. Matches the floor `cast storage` enforces.
44const MIN_STORAGE_LAYOUT_SOLC: semver::Version = semver::Version::new(0, 6, 5);
45/// First solc release with `--base-path` support.
46const BASE_PATH_SOLC: semver::Version = semver::Version::new(0, 6, 9);
47/// First solc release with `--no-import-callback` support.
48const NO_IMPORT_CALLBACK_SOLC: semver::Version = semver::Version::new(0, 8, 22);
49
50/// A resolved storage layout, or `None` if the contract has no layout we can use.
51///
52/// A `None` means the lookup concluded that there is nothing to decode with: the contract is
53/// unverified, is Vyper, failed to compile, or compiled to an empty layout. It is memoized for
54/// the rest of the run.
55type ExternalStorageLayout = Option<(String, Arc<StorageLayout>)>;
56
57/// A single address' lookup slot: `None` until the lookup completes, then its memoized result.
58type LookupSlot = Arc<Mutex<Option<ExternalStorageLayout>>>;
59
60/// Per-(chain, address) lookup slots, shared across the whole process.
61///
62/// The outer mutex only guards the map; the work itself happens while holding the inner mutex of a
63/// single entry, so lookups for different addresses still run concurrently while lookups for the
64/// same address wait for the first one to finish and then reuse its result.
65static LOOKUPS: LazyLock<Mutex<std::collections::HashMap<(u64, Address), LookupSlot>>> =
66    LazyLock::new(Default::default);
67
68/// Bounds expensive compiler installation and execution across parallel tests.
69static COMPILER: Mutex<()> = Mutex::new(());
70
71/// Makes cache publication paths unique within one process; the PID separates processes.
72static CACHE_TEMP_ID: AtomicU64 = AtomicU64::new(0);
73
74/// Resolves the storage layouts of `addresses` on `chain`, compiling verified sources as needed.
75///
76/// `fetch_sources` is only called for the addresses still unknown after the in-process and
77/// on-disk caches have been consulted, so a warm run makes no network requests at all. For each
78/// address it is handed, it returns that exact contract's verified source, or `None` if the block
79/// explorer conclusively reports that there is none.
80///
81/// An address `fetch_sources` leaves out is one it reached no conclusion about. Those are left
82/// unresolved rather than remembered as having no layout, so a later call tries again instead of
83/// letting one explorer outage disable decoding for the rest of the run.
84///
85/// Addresses without a usable layout are absent from the returned map.
86pub fn fetch_external_storage_layouts(
87    chain: Chain,
88    addresses: impl IntoIterator<Item = Address>,
89    timeout: Duration,
90    fetch_sources: impl FnOnce(&[Address], Duration) -> AddressMap<Option<Metadata>>,
91) -> AddressMap<(String, Arc<StorageLayout>)> {
92    let cache_dir = Config::foundry_etherscan_chain_cache_dir(chain);
93    resolve(chain.id(), cache_dir.as_deref(), addresses, timeout, fetch_sources)
94}
95
96/// [`fetch_external_storage_layouts`] with the cache location supplied, so tests can point it
97/// somewhere other than the user's home directory.
98fn resolve(
99    chain_id: u64,
100    cache_dir: Option<&Path>,
101    addresses: impl IntoIterator<Item = Address>,
102    timeout: Duration,
103    fetch_sources: impl FnOnce(&[Address], Duration) -> AddressMap<Option<Metadata>>,
104) -> AddressMap<(String, Arc<StorageLayout>)> {
105    let deadline = Instant::now().checked_add(timeout).unwrap_or_else(Instant::now);
106    let mut resolved = AddressMap::default();
107
108    // Claim a lookup slot per address. Sorting keeps the acquisition order identical in every
109    // thread, so holding several slots at once cannot deadlock.
110    let mut addresses = addresses.into_iter().collect::<Vec<_>>();
111    addresses.sort_unstable();
112    addresses.dedup();
113    let slots = {
114        let Some(mut lookups) = lock_until(&LOOKUPS, deadline) else {
115            warn!(target: "external-storage", "external storage lookup timed out");
116            return resolved;
117        };
118        addresses
119            .into_iter()
120            .map(|address| (address, lookups.entry((chain_id, address)).or_default().clone()))
121            .collect::<Vec<_>>()
122    };
123
124    let mut pending = Vec::new();
125    for (address, slot) in &slots {
126        // Waits for a concurrent lookup of the same address to finish, if any.
127        let Some(guard) = lock_until(slot, deadline) else {
128            warn!(target: "external-storage", %address, "external storage lookup timed out");
129            return resolved;
130        };
131        if let Some(cached) = &*guard {
132            if let Some((name, layout)) = cached {
133                resolved.insert(*address, (name.clone(), layout.clone()));
134            }
135            continue;
136        }
137        pending.push((*address, guard));
138    }
139
140    if pending.is_empty() {
141        return resolved;
142    }
143
144    // Serve whatever the disk cache already resolved, so the remaining work is only for addresses
145    // this machine has no layout for yet.
146    pending.retain_mut(|(address, guard)| {
147        let Some(cached) = read_cached_layout(cache_dir, *address) else {
148            return true;
149        };
150        resolved.insert(*address, cached.clone());
151        **guard = Some(Some(cached));
152        false
153    });
154
155    if pending.is_empty() {
156        return resolved;
157    }
158
159    let remaining = deadline.saturating_duration_since(Instant::now());
160    if remaining.is_zero() {
161        warn!(target: "external-storage", "external storage lookup timed out");
162        return resolved;
163    }
164    let sources =
165        fetch_sources(&pending.iter().map(|(address, _)| *address).collect::<Vec<_>>(), remaining);
166
167    for (address, mut guard) in pending {
168        let Some(source) = sources.get(&address) else {
169            // The lookup reached no conclusion. Leave the slot unresolved so the next call
170            // retries, rather than recording "no layout" on the strength of an outage.
171            continue;
172        };
173
174        let Some(metadata) = source else {
175            // Retry unverified contracts on the next run, when sources may be available.
176            *guard = Some(None);
177            continue;
178        };
179
180        let layout = compile_storage_layout(address, metadata, deadline);
181        if layout.is_none() && Instant::now() >= deadline {
182            // Exhausting this call's budget says nothing about whether the contract has a
183            // usable layout. Leave it unresolved so a later call can retry.
184            continue;
185        }
186        if let Some((name, layout)) = &layout {
187            resolved.insert(address, (name.clone(), layout.clone()));
188            write_cached_layout(cache_dir, address, name, layout);
189        }
190        // Compilation failures are memoized only for this run. Unlike an explicit unverified
191        // response, they may be transient and must not become a persistent negative entry.
192        *guard = Some(layout);
193    }
194
195    resolved
196}
197
198/// Compiles a verified source with `storageLayout` output enabled and extracts the layout.
199fn compile_storage_layout(
200    address: Address,
201    metadata: &Metadata,
202    deadline: Instant,
203) -> Option<(String, Arc<StorageLayout>)> {
204    if metadata.is_vyper() {
205        trace!(target: "external-storage", %address, "skipping vyper contract");
206        return None;
207    }
208
209    // Older solc has no `storageLayout` output at all, so compiling would cost a full solc run to
210    // produce nothing. `cast storage` bumps such contracts to `MIN_SOLC`; here there is no user
211    // asking about one specific contract, so just leave them undecoded.
212    match metadata.compiler_version() {
213        Ok(version) if version < MIN_STORAGE_LAYOUT_SOLC => {
214            trace!(target: "external-storage", %address, %version, "solc too old for storage layouts");
215            return None;
216        }
217        Ok(_) => {}
218        Err(err) => {
219            warn!(target: "external-storage", %address, %err, "could not read compiler version");
220            return None;
221        }
222    }
223
224    let sources = metadata
225        .sources()
226        .into_iter()
227        .map(|(path, source)| (PathBuf::from(path), Source::new(source.content)))
228        .collect::<Sources>();
229    if sources.is_empty() {
230        trace!(target: "external-storage", %address, "verified metadata has no sources");
231        return None;
232    }
233    let Some(_compiler) = lock_until(&COMPILER, deadline) else {
234        warn!(target: "external-storage", %address, "external storage compilation timed out");
235        return None;
236    };
237
238    // Compile standard JSON with every source supplied inline in an empty sandbox. Newer solc
239    // versions disable the filesystem import callback explicitly; the empty working/base directory
240    // prevents older versions from resolving omitted sources.
241    let version = metadata.compiler_version().ok()?;
242    let mut settings = match metadata.settings() {
243        Ok(settings) => settings,
244        Err(err) => {
245            warn!(target: "external-storage", %address, %err, "failed to read compiler settings");
246            return None;
247        }
248    };
249    settings.output_selection =
250        OutputSelection::common_output_selection(["storageLayout".to_string()]);
251    let input = SolcInput::new(SolcLanguage::Solidity, sources, settings).sanitized(&version);
252    let svm_version = semver::Version::new(version.major, version.minor, version.patch);
253    let solc = match Solc::find_svm_installed_version(&svm_version) {
254        Ok(Some(solc)) => solc,
255        Ok(None) => {
256            let remaining = deadline.saturating_duration_since(Instant::now());
257            let installed =
258                crate::block_on(tokio::time::timeout(remaining, Solc::install(&svm_version)));
259            match installed {
260                Ok(Ok(solc)) => solc,
261                Ok(Err(err)) => {
262                    warn!(target: "external-storage", %address, %err, "failed to install compiler");
263                    return None;
264                }
265                Err(_) => {
266                    warn!(target: "external-storage", %address, "compiler installation timed out");
267                    return None;
268                }
269            }
270        }
271        Err(err) => {
272            warn!(target: "external-storage", %address, %err, "failed to find compiler");
273            return None;
274        }
275    };
276    let remaining = deadline.saturating_duration_since(Instant::now());
277    let output = match run_solc(&solc, &version, &input, remaining) {
278        Ok(output) => output,
279        Err(err) => {
280            warn!(target: "external-storage", %address, %err, "failed to compile contract");
281            return None;
282        }
283    };
284
285    let name = metadata.contract_name.clone();
286    let mut matches = output
287        .contracts
288        .values()
289        .filter_map(|contracts| contracts.get(&name))
290        .filter(|contract| !contract.storage_layout.storage.is_empty());
291    let layout = matches.next().map(|contract| contract.storage_layout.clone());
292    if matches.next().is_some() {
293        warn!(target: "external-storage", %address, %name, "multiple artifacts match contract name");
294        return None;
295    }
296
297    let Some(layout) = layout else {
298        warn!(target: "external-storage", %address, %name, "no storage layout in compiled artifacts");
299        return None;
300    };
301
302    Some((name, Arc::new(layout)))
303}
304
305/// Runs solc in an empty directory and terminates it if the remaining lookup budget expires.
306fn run_solc(
307    solc: &Solc,
308    version: &semver::Version,
309    input: &SolcInput,
310    timeout: Duration,
311) -> Result<CompilerOutput, String> {
312    if timeout.is_zero() {
313        return Err("compilation timed out".to_string());
314    }
315
316    let sandbox = tempfile::tempdir().map_err(|err| err.to_string())?;
317    let mut stdin = tempfile::tempfile().map_err(|err| err.to_string())?;
318    serde_json::to_writer(&mut stdin, input).map_err(|err| err.to_string())?;
319    stdin.seek(SeekFrom::Start(0)).map_err(|err| err.to_string())?;
320    let mut stdout = tempfile::tempfile().map_err(|err| err.to_string())?;
321    let mut stderr = tempfile::tempfile().map_err(|err| err.to_string())?;
322
323    let mut command = Command::new(&solc.solc);
324    command.arg("--standard-json").current_dir(sandbox.path());
325    if version >= &BASE_PATH_SOLC {
326        command.arg("--base-path").arg(sandbox.path());
327    }
328    if version >= &NO_IMPORT_CALLBACK_SOLC {
329        command.arg("--no-import-callback");
330    }
331    command
332        .stdin(Stdio::from(stdin))
333        .stdout(Stdio::from(stdout.try_clone().map_err(|err| err.to_string())?))
334        .stderr(Stdio::from(stderr.try_clone().map_err(|err| err.to_string())?));
335
336    let mut child = command.spawn().map_err(|err| err.to_string())?;
337    let status = match child.wait_timeout(timeout) {
338        Ok(Some(status)) => status,
339        Ok(None) => {
340            let _ = child.kill();
341            let _ = child.wait();
342            return Err("compilation timed out".to_string());
343        }
344        Err(err) => {
345            let _ = child.kill();
346            let _ = child.wait();
347            return Err(err.to_string());
348        }
349    };
350    if !status.success() {
351        stderr.seek(SeekFrom::Start(0)).map_err(|err| err.to_string())?;
352        let mut message = String::new();
353        stderr.read_to_string(&mut message).map_err(|err| err.to_string())?;
354        return Err(if message.trim().is_empty() {
355            format!("solc exited with {status}")
356        } else {
357            message
358        });
359    }
360
361    stdout.seek(SeekFrom::Start(0)).map_err(|err| err.to_string())?;
362    serde_json::from_reader(stdout).map_err(|err| err.to_string())
363}
364
365/// Disk representation of a resolved lookup. Cleared by `forge cache clean`.
366#[derive(Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368struct CachedStorageLayout {
369    /// Bumped whenever a change makes previously written entries wrong. Entries written by any
370    /// other version are ignored, so a fix doesn't need users to clear their cache by hand.
371    version: u32,
372    contract_name: String,
373    storage_layout: StorageLayout,
374}
375
376/// Current [`CachedStorageLayout`] format.
377const CACHE_VERSION: u32 = 2;
378
379/// Path of the cache entry for `address`.
380fn cache_path(cache_dir: &Path, address: Address) -> PathBuf {
381    cache_dir.join("storage_layouts").join(format!("{address}.json"))
382}
383
384/// Reads the layout a previous run resolved for `address`, if there is one.
385fn read_cached_layout(
386    cache_dir: Option<&Path>,
387    address: Address,
388) -> Option<(String, Arc<StorageLayout>)> {
389    let path = cache_path(cache_dir?, address);
390    let cached = fs::read_json_file::<CachedStorageLayout>(&path).ok()?;
391    if cached.version != CACHE_VERSION {
392        trace!(target: "external-storage", %address, cached.version, "ignoring stale cache entry");
393        return None;
394    }
395    trace!(target: "external-storage", %address, "using cached storage layout");
396    Some((cached.contract_name, Arc::new(cached.storage_layout)))
397}
398
399/// Persists a resolved layout so later runs can skip the fetch and the compile.
400///
401/// The entry is written to a temporary file and renamed into place, so parallel tests writing the
402/// same address cannot leave a reader with a half-written file.
403fn write_cached_layout(
404    cache_dir: Option<&Path>,
405    address: Address,
406    name: &str,
407    layout: &StorageLayout,
408) {
409    let Some(cache_dir) = cache_dir else { return };
410    let path = cache_path(cache_dir, address);
411    let Some(parent) = path.parent() else { return };
412    if let Err(err) = std::fs::create_dir_all(parent) {
413        warn!(target: "external-storage", %address, %err, "failed to create storage layout cache");
414        return;
415    }
416
417    let cached = CachedStorageLayout {
418        version: CACHE_VERSION,
419        contract_name: name.to_string(),
420        storage_layout: layout.clone(),
421    };
422
423    // A uniquely created file prevents threads and processes from sharing a writer. Persisting it
424    // in the destination directory publishes a complete JSON document with one atomic rename.
425    let tmp_path = path.with_extension(format!(
426        "json.{}.{}.tmp",
427        std::process::id(),
428        CACHE_TEMP_ID.fetch_add(1, Ordering::Relaxed)
429    ));
430    match std::fs::OpenOptions::new().write(true).create_new(true).open(&tmp_path) {
431        Ok(_) => {}
432        Err(err) => {
433            warn!(target: "external-storage", %address, %err, "failed to create cache temporary file");
434            return;
435        }
436    }
437    let write = fs::write_json_file(&tmp_path, &cached).and_then(|()| {
438        std::fs::rename(&tmp_path, &path)
439            .map_err(|err| crate::errors::FsPathError::write(err, path.as_path()))
440    });
441    if let Err(err) = write {
442        warn!(target: "external-storage", %address, %err, "failed to cache storage layout");
443        let _ = std::fs::remove_file(&tmp_path);
444    }
445}
446
447/// Acquires a lock without exceeding the caller's deadline.
448///
449/// Poisoned locks are recovered: everything guarded this way only holds memoized lookup state.
450pub fn lock_until<T>(mutex: &Mutex<T>, deadline: Instant) -> Option<MutexGuard<'_, T>> {
451    loop {
452        match mutex.try_lock() {
453            Ok(guard) => return Some(guard),
454            Err(TryLockError::Poisoned(err)) => return Some(err.into_inner()),
455            Err(TryLockError::WouldBlock) => {
456                let remaining = deadline.saturating_duration_since(Instant::now());
457                if remaining.is_zero() {
458                    return None;
459                }
460                std::thread::sleep(remaining.min(Duration::from_millis(1)));
461            }
462        }
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use foundry_block_explorers::contract::SourceCodeMetadata;
470
471    #[cfg(unix)]
472    use std::os::unix::fs::PermissionsExt;
473
474    /// [`LOOKUPS`] is keyed by chain, so giving every test its own chain id keeps them from
475    /// seeing each other's memoized results.
476    fn next_chain_id() -> u64 {
477        static NEXT: AtomicU64 = AtomicU64::new(1);
478        NEXT.fetch_add(1, Ordering::Relaxed)
479    }
480
481    #[test]
482    fn round_trips_a_resolved_layout() {
483        let cache_dir = tempfile::tempdir().unwrap();
484        let address = Address::with_last_byte(1);
485
486        // Nothing cached yet.
487        assert!(read_cached_layout(Some(cache_dir.path()), address).is_none());
488
489        let layout = StorageLayout::default();
490        write_cached_layout(Some(cache_dir.path()), address, "Counter", &layout);
491
492        let (name, cached) = read_cached_layout(Some(cache_dir.path()), address).unwrap();
493        assert_eq!(name, "Counter");
494        assert_eq!(*cached, layout);
495    }
496
497    #[test]
498    fn ignores_cache_entries_written_by_another_version() {
499        let cache_dir = tempfile::tempdir().unwrap();
500        let address = Address::with_last_byte(1);
501        write_cached_layout(Some(cache_dir.path()), address, "Counter", &StorageLayout::default());
502
503        let path = cache_path(cache_dir.path(), address);
504        let stale = std::fs::read_to_string(&path)
505            .unwrap()
506            .replace(&format!("\"version\":{CACHE_VERSION}"), "\"version\":0");
507        std::fs::write(&path, stale).unwrap();
508
509        assert!(read_cached_layout(Some(cache_dir.path()), address).is_none());
510    }
511
512    #[test]
513    fn write_leaves_no_temporary_files_behind() {
514        let cache_dir = tempfile::tempdir().unwrap();
515        let address = Address::with_last_byte(1);
516        write_cached_layout(Some(cache_dir.path()), address, "Counter", &StorageLayout::default());
517
518        let entries = std::fs::read_dir(cache_dir.path().join("storage_layouts"))
519            .unwrap()
520            .map(|entry| entry.unwrap().file_name())
521            .collect::<Vec<_>>();
522        assert_eq!(entries, [format!("{address}.json").as_str()]);
523    }
524
525    #[test]
526    fn serves_the_disk_cache_without_looking_anything_up() {
527        let cache_dir = tempfile::tempdir().unwrap();
528        let chain_id = next_chain_id();
529        let cached = Address::with_last_byte(1);
530        let fresh = Address::with_last_byte(2);
531        write_cached_layout(Some(cache_dir.path()), cached, "Counter", &StorageLayout::default());
532
533        let mut asked_for = Vec::new();
534        let resolved = resolve(
535            chain_id,
536            Some(cache_dir.path()),
537            [cached, fresh],
538            Duration::from_secs(1),
539            |addresses, _| {
540                asked_for.extend_from_slice(addresses);
541                AddressMap::default()
542            },
543        );
544
545        // Only the uncached address reaches the lookup, and the cached one still comes back.
546        assert_eq!(asked_for, [fresh]);
547        assert_eq!(resolved.keys().copied().collect::<Vec<_>>(), [cached]);
548        assert_eq!(resolved[&cached].0, "Counter");
549    }
550
551    #[test]
552    fn remembers_a_conclusive_miss_for_the_rest_of_the_run() {
553        let cache_dir = tempfile::tempdir().unwrap();
554        let chain_id = next_chain_id();
555        let address = Address::with_last_byte(1);
556
557        let mut lookups = 0;
558        let mut unverified = |addresses: &[Address], _: Duration| {
559            lookups += 1;
560            addresses.iter().map(|address| (*address, None)).collect::<AddressMap<_>>()
561        };
562
563        assert!(
564            resolve(
565                chain_id,
566                Some(cache_dir.path()),
567                [address],
568                Duration::from_secs(1),
569                &mut unverified,
570            )
571            .is_empty()
572        );
573        assert!(
574            resolve(
575                chain_id,
576                Some(cache_dir.path()),
577                [address],
578                Duration::from_secs(1),
579                &mut unverified,
580            )
581            .is_empty()
582        );
583        assert_eq!(lookups, 1, "an unverified contract should only be looked up once");
584        assert!(!cache_path(cache_dir.path(), address).exists());
585    }
586
587    #[test]
588    fn retries_an_address_the_lookup_reached_no_conclusion_about() {
589        let cache_dir = tempfile::tempdir().unwrap();
590        let chain_id = next_chain_id();
591        let address = Address::with_last_byte(1);
592
593        // An explorer outage: the lookup answers for nothing it was asked about.
594        let mut lookups = 0;
595        let mut unavailable = |_: &[Address], _: Duration| {
596            lookups += 1;
597            AddressMap::default()
598        };
599
600        assert!(
601            resolve(
602                chain_id,
603                Some(cache_dir.path()),
604                [address],
605                Duration::from_secs(1),
606                &mut unavailable,
607            )
608            .is_empty()
609        );
610        assert!(
611            resolve(
612                chain_id,
613                Some(cache_dir.path()),
614                [address],
615                Duration::from_secs(1),
616                &mut unavailable,
617            )
618            .is_empty()
619        );
620        assert_eq!(lookups, 2, "an outage must not be remembered as \"no layout\"");
621    }
622
623    #[test]
624    fn waiting_for_an_inflight_lookup_respects_the_timeout() {
625        let chain_id = next_chain_id();
626        let address = Address::with_last_byte(1);
627        let slot = {
628            let mut lookups = LOOKUPS.lock().unwrap();
629            lookups.entry((chain_id, address)).or_default().clone()
630        };
631        let _inflight = slot.lock().unwrap();
632        let started = Instant::now();
633        let mut fetched = false;
634
635        let result = resolve(chain_id, None, [address], Duration::from_millis(20), |_, _| {
636            fetched = true;
637            AddressMap::default()
638        });
639
640        assert!(result.is_empty());
641        assert!(!fetched);
642        assert!(started.elapsed() < Duration::from_secs(1));
643    }
644
645    #[cfg(unix)]
646    #[test]
647    fn solc_is_killed_when_compilation_times_out() {
648        let dir = tempfile::tempdir().unwrap();
649        let path = dir.path().join("solc");
650        std::fs::write(&path, "#!/bin/sh\nexec sleep 10\n").unwrap();
651        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
652        let solc = Solc {
653            solc: path,
654            version: NO_IMPORT_CALLBACK_SOLC,
655            base_path: None,
656            allow_paths: Default::default(),
657            include_paths: Default::default(),
658            extra_args: Vec::new(),
659        };
660        let input = SolcInput::new(SolcLanguage::Solidity, Default::default(), Default::default());
661        let started = Instant::now();
662
663        assert!(
664            run_solc(&solc, &NO_IMPORT_CALLBACK_SOLC, &input, Duration::from_millis(20)).is_err()
665        );
666        assert!(started.elapsed() < Duration::from_secs(1));
667    }
668
669    #[cfg(unix)]
670    #[test]
671    fn solc_runs_in_an_empty_sandbox_with_imports_disabled() {
672        let dir = tempfile::tempdir().unwrap();
673        let path = dir.path().join("solc");
674        std::fs::write(
675            &path,
676            "#!/bin/sh\ncase \" $* \" in *\" --no-import-callback \"*) ;; *) exit 1;; esac\n[ -z \"$(ls -A)\" ] || exit 1\nprintf '{\"contracts\":{}}'\n",
677        )
678        .unwrap();
679        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
680        let solc = Solc {
681            solc: path,
682            version: NO_IMPORT_CALLBACK_SOLC,
683            base_path: None,
684            allow_paths: Default::default(),
685            include_paths: Default::default(),
686            extra_args: Vec::new(),
687        };
688        let input = SolcInput::new(SolcLanguage::Solidity, Default::default(), Default::default());
689
690        assert!(run_solc(&solc, &NO_IMPORT_CALLBACK_SOLC, &input, Duration::from_secs(1)).is_ok());
691    }
692
693    fn source_less_metadata() -> Metadata {
694        Metadata {
695            source_code: SourceCodeMetadata::Sources(Default::default()),
696            abi: "[]".to_string(),
697            contract_name: "MissingSources".to_string(),
698            compiler_version: "v0.8.30".to_string(),
699            optimization_used: 0,
700            runs: 0,
701            constructor_arguments: Default::default(),
702            evm_version: String::new(),
703            library: String::new(),
704            license_type: String::new(),
705            proxy: 0,
706            implementation: None,
707            swarm_source: String::new(),
708        }
709    }
710
711    #[test]
712    fn retries_after_the_compilation_budget_expires() {
713        let chain_id = next_chain_id();
714        let address = Address::with_last_byte(1);
715        let metadata = Metadata {
716            source_code: SourceCodeMetadata::SourceCode(
717                "pragma solidity ^0.8.30; contract Counter { uint256 public count; }".to_string(),
718            ),
719            contract_name: "Counter".to_string(),
720            ..source_less_metadata()
721        };
722        // Keep compilation from progressing after fetching consumes the shared budget.
723        let _compiler = COMPILER.lock().unwrap();
724        let result =
725            resolve(chain_id, None, [address], Duration::from_millis(20), |_, remaining| {
726                std::thread::sleep(remaining);
727                [(address, Some(metadata))].into_iter().collect()
728            });
729        assert!(result.is_empty());
730
731        let mut retried = false;
732        resolve(chain_id, None, [address], Duration::from_secs(1), |addresses, _| {
733            assert_eq!(addresses, [address]);
734            retried = true;
735            AddressMap::default()
736        });
737        assert!(retried, "a compilation timeout must not be memoized as no layout");
738    }
739
740    #[test]
741    fn source_less_metadata_is_rejected_without_compiling() {
742        let metadata = source_less_metadata();
743        assert!(compile_storage_layout(Address::ZERO, &metadata, Instant::now()).is_none());
744    }
745}