Skip to main content

foundry_common/
namespaced_storage.rs

1//! ERC-7201 namespaced storage layouts.
2//!
3//! solc's `storageLayout` output only covers conventional storage. Namespaces declared with
4//! `@custom:storage-location erc7201:<id>` are computed with Solar and appended to it, so storage
5//! decoding labels namespaced fields as `<id>.<field>`.
6
7use alloy_primitives::U256;
8use eyre::Result;
9use foundry_compilers::{
10    ArtifactId, ProjectCompileOutput,
11    artifacts::{Remapping, Sources, StorageLayout},
12};
13use path_slash::PathExt;
14use solar::{
15    ast::NatSpecKind,
16    interface::{
17        ColorChoice, Session,
18        config::{CompileOpts, ImportRemapping},
19        source_map::{FileLoader, FileName, SourceMap},
20    },
21    sema::{Compiler, CompilerRef, Gcx, hir, output::StorageLayoutOutput},
22};
23use std::{
24    collections::{BTreeMap, HashMap},
25    io,
26    ops::ControlFlow,
27    path::{Path, PathBuf},
28    sync::Arc,
29};
30
31/// Returns the storage layouts of `output`'s artifacts with ERC-7201 namespaces appended.
32///
33/// See [`extend_storage_layouts`].
34pub fn project_storage_layouts(
35    output: &mut ProjectCompileOutput,
36    root: &Path,
37) -> Vec<(ArtifactId, StorageLayout)> {
38    let layouts = output
39        .artifact_ids()
40        .filter_map(|(id, artifact)| {
41            artifact.storage_layout.as_ref().map(|layout| (id, layout.clone()))
42        })
43        .collect::<Vec<_>>();
44    extend_storage_layouts(output.parser_mut().solc_mut().compiler_mut(), root, layouts)
45}
46
47/// Returns `layouts` with the ERC-7201 namespaces of each contract appended.
48///
49/// `compiler` must contain the parsed sources of the artifacts; it is lowered if necessary. Source
50/// names are made relative to `root` to match solc's. Namespaces are optional decoding metadata, so
51/// failures are logged and leave the affected layouts unchanged.
52pub fn extend_storage_layouts(
53    compiler: &mut Compiler,
54    root: &Path,
55    layouts: impl IntoIterator<Item = (ArtifactId, StorageLayout)>,
56) -> Vec<(ArtifactId, StorageLayout)> {
57    let mut layouts = layouts.into_iter().collect::<Vec<_>>();
58    if layouts.is_empty() || !has_annotations(compiler) {
59        return layouts;
60    }
61    let result = compiler.enter_mut(|compiler| -> Result<()> {
62        lower(compiler)?;
63        let gcx = compiler.gcx();
64        let contracts = contracts_by_source(gcx, root);
65        for (id, layout) in &mut layouts {
66            let source = id.source.strip_prefix(root).unwrap_or(&id.source).to_slash_lossy();
67            let Some(&contract) = contracts.get(&(source.into_owned(), id.name.clone())) else {
68                continue;
69            };
70            if let Err(err) = extend_storage_layout(layout, gcx, root, contract) {
71                warn!(target: "erc7201", artifact = %id.identifier(), %err, "skipping ERC-7201 namespaces");
72            }
73        }
74        Ok(())
75    });
76    if let Err(err) = result {
77        warn!(target: "erc7201", %err, "skipping ERC-7201 namespaces");
78    }
79    layouts
80}
81
82/// Appends the ERC-7201 namespaces of `contract` and its bases to its storage layout.
83///
84/// Namespaced fields are labeled `<namespace>.<field>`. Malformed annotations, duplicate namespace
85/// roots, and errors emitted while computing types are errors, in which case `layout` is unchanged.
86pub fn extend_storage_layout(
87    layout: &mut StorageLayout,
88    gcx: Gcx<'_>,
89    root: &Path,
90    contract: hir::ContractId,
91) -> Result<()> {
92    // Only errors emitted for this contract count; earlier ones may come from unrelated sources.
93    let errors = gcx.sess.dcx.err_count();
94    let namespaces = collect_storage_layouts(gcx, root, contract)?;
95    if gcx.sess.dcx.err_count() > errors {
96        return Err(solar_error(gcx.sess));
97    }
98    merge_storage_namespaces(layout, namespaces)
99}
100
101/// Lowers the parsed sources to HIR unless that has already happened.
102///
103/// Only the HIR and the types reachable from namespaced structs are needed, so this does not run
104/// full semantic analysis; solc has already validated the sources. Diagnostics emitted while
105/// lowering unrelated sources are not errors here.
106pub fn lower(compiler: &mut CompilerRef<'_>) -> Result<()> {
107    if compiler.gcx().stage().is_none_or(|stage| stage.is_parsing())
108        && !matches!(compiler.lower_asts(), Ok(ControlFlow::Continue(())))
109    {
110        return Err(solar_error(compiler.sess()));
111    }
112    Ok(())
113}
114
115/// Parses Standard JSON `sources`, keyed by source unit name, without touching the filesystem.
116///
117/// Returns `None` if no source declares an ERC-7201 namespace.
118pub fn parse_standard_json(sources: &Sources, remappings: &[Remapping]) -> Option<Compiler> {
119    if !sources.values().any(|source| has_annotation(&source.content)) {
120        return None;
121    }
122    let source_map = Arc::new(SourceMap::empty());
123    source_map.set_file_loader(NoFileLoader);
124    let opts = CompileOpts {
125        standard_json: true,
126        import_remappings: remappings
127            .iter()
128            .filter_map(|remapping| remapping.to_string().parse::<ImportRemapping>().ok())
129            .collect(),
130        ..Default::default()
131    };
132    let sess = Session::builder()
133        .source_map(source_map)
134        .with_buffer_emitter(ColorChoice::Never)
135        .opts(opts)
136        .build();
137    let mut compiler = Compiler::new(sess);
138    let _ = compiler.enter_mut(|compiler| -> solar::interface::Result {
139        let mut pcx = compiler.parse();
140        pcx.par_load_files_with_contents(
141            sources
142                .iter()
143                .map(|(path, source)| (path.clone(), source.content.as_str().to_owned()))
144                .collect::<Vec<_>>(),
145        )?;
146        pcx.parse();
147        Ok(())
148    });
149    Some(compiler)
150}
151
152/// Returns the contract `name` declared in the source `path`, relative to `root`.
153pub fn find_contract(
154    gcx: Gcx<'_>,
155    root: &Path,
156    path: &Path,
157    name: &str,
158) -> Option<hir::ContractId> {
159    let source = path.strip_prefix(root).unwrap_or(path).to_slash_lossy().into_owned();
160    contracts_by_source(gcx, root).remove(&(source, name.to_owned()))
161}
162
163/// The NatSpec tag content that marks a namespace; cheap to search for in source text.
164const ANNOTATION: &str = "erc7201";
165
166/// A storage namespace declared by a struct in a contract or one of its bases.
167#[derive(Clone, Debug)]
168struct StorageNamespace {
169    /// The identifier following `erc7201:` in the storage-location annotation.
170    id: String,
171    /// The fully qualified name of the declaring struct.
172    declaration: String,
173    /// The ERC-7201 root slot.
174    root: U256,
175    /// The struct's fields at absolute slots, with nested members at relative slots.
176    ///
177    /// Type IDs are scoped to this namespace so they cannot alias solc-generated IDs.
178    /// AST IDs originate from Solar and are not solc AST IDs.
179    layout: StorageLayout,
180}
181
182/// Rejects filesystem access, so Standard JSON imports only resolve to the supplied sources.
183struct NoFileLoader;
184
185impl FileLoader for NoFileLoader {
186    fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf> {
187        Ok(path.to_path_buf())
188    }
189
190    fn load_stdin(&self) -> io::Result<String> {
191        Err(io::ErrorKind::Unsupported.into())
192    }
193
194    fn load_file(&self, _path: &Path) -> io::Result<String> {
195        Err(io::ErrorKind::NotFound.into())
196    }
197
198    fn load_binary_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
199        Err(io::ErrorKind::NotFound.into())
200    }
201}
202
203/// Returns whether any parsed source mentions an ERC-7201 storage location.
204pub fn has_annotations(compiler: &Compiler) -> bool {
205    compiler.sess().source_map().files().iter().any(|file| has_annotation(&file.src))
206}
207
208/// Returns whether `source` mentions an ERC-7201 storage location.
209fn has_annotation(source: &str) -> bool {
210    source.contains(ANNOTATION)
211}
212
213/// Maps `(source, name)` to contracts, with sources relative to `root` and slash-separated.
214fn contracts_by_source(gcx: Gcx<'_>, root: &Path) -> HashMap<(String, String), hir::ContractId> {
215    gcx.hir
216        .contract_ids()
217        .filter_map(|id| {
218            let contract = gcx.hir.contract(id);
219            let FileName::Real(path) = &gcx.hir.source(contract.source).file.name else {
220                return None;
221            };
222            let source = path.strip_prefix(root).unwrap_or(path).to_slash_lossy().into_owned();
223            Some(((source, contract.name.to_string()), id))
224        })
225        .collect()
226}
227
228/// Returns namespaces declared in `contract` and its linearized bases, once per declaration.
229///
230/// Unrelated contracts, imported libraries, and file-level structs are excluded: importing a
231/// declaration does not establish that the selected contract uses its storage. Unknown
232/// storage-location schemes are ignored; malformed ERC-7201 annotations are errors.
233fn collect_storage_layouts(
234    gcx: Gcx<'_>,
235    root: &Path,
236    contract: hir::ContractId,
237) -> Result<Vec<StorageNamespace>> {
238    let mut namespaces = Vec::new();
239    for &base in gcx.hir.contract(contract).linearized_bases.iter().rev() {
240        let contract = gcx.hir.contract(base);
241        for id in contract.items.iter().filter_map(|item| item.as_struct()) {
242            let strukt = gcx.hir.strukt(id);
243            let Some(namespace) = storage_namespace(gcx, strukt)? else { continue };
244            // Match solc's project-relative source names.
245            let contract_name = match &gcx.hir.source(contract.source).file.name {
246                FileName::Real(path) => format!(
247                    "{}:{}",
248                    path.strip_prefix(root).unwrap_or(path).to_slash_lossy(),
249                    contract.name
250                ),
251                _ => gcx.contract_fully_qualified_name(base).to_string(),
252            };
253            let declaration = format!("{contract_name}.{}", strukt.name);
254            let root = crate::erc7201(namespace).into();
255            let mut output = gcx.storage_layout_for_struct(id, root);
256            scope_layout(&mut output, namespace, &contract_name);
257            namespaces.push(StorageNamespace {
258                id: namespace.to_owned(),
259                declaration,
260                root,
261                layout: serde_json::from_value(serde_json::to_value(output)?)?,
262            });
263        }
264    }
265    Ok(namespaces)
266}
267
268/// Returns the ERC-7201 namespace declared by a struct's `@custom:storage-location` tag.
269fn storage_namespace<'a>(gcx: Gcx<'a>, strukt: &hir::Struct<'_>) -> Result<Option<&'a str>> {
270    let mut namespace = None;
271    for doc in gcx.hir.doc(strukt.doc).ast_comments().iter() {
272        for tag in doc.natspec.iter() {
273            if let NatSpecKind::Custom { name } = tag.kind
274                && name.as_str() == "storage-location"
275                && let Some(id) = parse_namespace(tag.content())?
276                && namespace.replace(id).is_some()
277            {
278                eyre::bail!("multiple ERC-7201 annotations on struct `{}`", strukt.name);
279            }
280        }
281    }
282    Ok(namespace)
283}
284
285fn parse_namespace(annotation: &str) -> Result<Option<&str>> {
286    let annotation = annotation.trim();
287    let Some(id) = annotation.strip_prefix("erc7201:") else {
288        eyre::ensure!(
289            annotation.split_whitespace().next() != Some(ANNOTATION),
290            "expected `erc7201:<namespace>` storage location"
291        );
292        return Ok(None);
293    };
294    eyre::ensure!(
295        !id.is_empty() && !id.contains(char::is_whitespace),
296        "ERC-7201 namespace must be nonempty and contain no whitespace"
297    );
298    Ok(Some(id))
299}
300
301/// Scopes type IDs to `namespace` and attributes every entry to `contract`.
302fn scope_layout(output: &mut StorageLayoutOutput, namespace: &str, contract: &str) {
303    let scope = |id: &mut String| *id = format!("erc7201({namespace})::{id}");
304    for entry in &mut output.storage {
305        scope(&mut entry.r#type);
306        entry.contract = contract.to_owned();
307    }
308    let Some(types) = &mut output.types else { return };
309    *types = std::mem::take(types)
310        .into_iter()
311        .map(|(mut id, mut ty)| {
312            for reference in [&mut ty.base, &mut ty.key, &mut ty.value].into_iter().flatten() {
313                scope(reference);
314            }
315            for member in &mut ty.members {
316                scope(&mut member.r#type);
317                member.contract = contract.to_owned();
318            }
319            scope(&mut id);
320            (id, ty)
321        })
322        .collect();
323}
324
325/// Merges namespaces, rejecting duplicate declarations before `layout` is modified.
326///
327/// Type IDs are scoped to their namespace, so they can't collide with each other or with solc's.
328fn merge_storage_namespaces(
329    layout: &mut StorageLayout,
330    namespaces: Vec<StorageNamespace>,
331) -> Result<()> {
332    let mut declarations = BTreeMap::new();
333    for namespace in &namespaces {
334        if let Some(previous) = declarations.insert(namespace.root, &namespace.declaration) {
335            eyre::bail!(
336                "Duplicate ERC-7201 namespace `{}` in `{previous}` and `{}`",
337                namespace.id,
338                namespace.declaration
339            );
340        }
341    }
342    for namespace in namespaces {
343        layout.types.extend(namespace.layout.types);
344        for mut entry in namespace.layout.storage {
345            entry.label = format!("{}.{}", namespace.id, entry.label);
346            layout.storage.push(entry);
347        }
348    }
349    Ok(())
350}
351
352fn solar_error(sess: &Session) -> eyre::Error {
353    let diagnostics = sess.dcx.emitted_diagnostics().map(|d| d.to_string()).unwrap_or_default();
354    eyre::eyre!("Solar failed while inspecting ERC-7201 storage\n{diagnostics}")
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    /// Lowers `compiler` and collects the namespaces of `name` in `path`.
362    fn erc7201_storage_layouts(
363        compiler: &mut Compiler,
364        root: &Path,
365        path: &Path,
366        name: Option<&str>,
367    ) -> Result<Vec<StorageNamespace>> {
368        compiler.enter_mut(|compiler| {
369            lower(compiler)?;
370            let gcx = compiler.gcx();
371            let contract = find_contract(gcx, root, path, name.unwrap()).unwrap();
372            let namespaces = collect_storage_layouts(gcx, root, contract)?;
373            if gcx.sess.dcx.has_errors().is_err() {
374                return Err(solar_error(gcx.sess));
375            }
376            Ok(namespaces)
377        })
378    }
379
380    fn parser_at(source: &str, path: &str) -> Compiler {
381        let mut compiler = Compiler::new(Session::builder().with_test_emitter().build());
382        compiler.enter_mut(|compiler| {
383            let file =
384                compiler.sess().source_map().new_source_file(PathBuf::from(path), source).unwrap();
385            let mut parser = compiler.parse();
386            parser.add_file(file);
387            parser.parse();
388        });
389        compiler
390    }
391
392    fn layouts(source: &str, contract: &str) -> Result<Vec<StorageNamespace>> {
393        erc7201_storage_layouts(
394            &mut parser_at(source, "test.sol"),
395            Path::new(""),
396            Path::new("test.sol"),
397            Some(contract),
398        )
399    }
400
401    #[test]
402    fn project_relative_source_names() {
403        let mut parser = parser_at(
404            r#"contract C {
405            struct Inner { uint256 x; }
406            /// @custom:storage-location erc7201:example
407            struct Data { Inner inner; }
408        }"#,
409            "/project/src/test.sol",
410        );
411        let namespaces = erc7201_storage_layouts(
412            &mut parser,
413            Path::new("/project"),
414            Path::new("/project/src/test.sol"),
415            Some("C"),
416        )
417        .unwrap();
418        let namespace = &namespaces[0];
419        assert_eq!(namespace.declaration, "src/test.sol:C.Data");
420        assert_eq!(namespace.layout.storage[0].contract, "src/test.sol:C");
421        let ty = &namespace.layout.types[&namespace.layout.storage[0].storage_type];
422        assert_eq!(ty.other["members"][0]["contract"], "src/test.sol:C");
423    }
424
425    #[test]
426    fn repeated_inspection() {
427        let mut parser = parser_at(
428            r#"contract C {
429            /// @custom:storage-location erc7201:example
430            struct Data { uint256 value; }
431        }"#,
432            "test.sol",
433        );
434        let mut inspect = || {
435            erc7201_storage_layouts(&mut parser, Path::new(""), Path::new("test.sol"), Some("C"))
436                .unwrap()
437        };
438        assert_eq!(inspect()[0].layout, inspect()[0].layout);
439    }
440
441    #[test]
442    fn inherited_namespaces_and_type_graph() {
443        let namespaces = layouts(
444            r#"
445            type Amount is uint128;
446            contract Base {
447                enum State { Off, On }
448                struct Nested { uint128 x; bool y; }
449                /** @custom:storage-location erc7201:example.base */
450                struct Data {
451                    address owner;
452                    bool paused;
453                    Nested nested;
454                    uint128[3] fixedValues;
455                    mapping(address => Nested[]) accounts;
456                    bytes data;
457                    string name;
458                    Amount amount;
459                    State state;
460                    function() external callback;
461                }
462            }
463            contract Left is Base {}
464            contract Right is Base {}
465            contract Derived is Left, Right {
466                /// @custom:storage-location erc7201:example.derived
467                struct OtherData { uint256 value; }
468            }
469            contract Unrelated {
470                /// @custom:storage-location erc7201:example.base
471                struct Data { uint256 ignored; }
472            }
473        "#,
474            "Derived",
475        )
476        .unwrap();
477        assert_eq!(
478            namespaces.iter().map(|n| n.id.as_str()).collect::<Vec<_>>(),
479            ["example.base", "example.derived"]
480        );
481        assert_eq!(namespaces[0].declaration, "test.sol:Base.Data");
482        let layout = &namespaces[0].layout;
483        let root = namespaces[0].root;
484        assert_eq!(
485            layout
486                .storage
487                .iter()
488                .map(|s| (s.slot.parse::<U256>().unwrap() - root, s.offset))
489                .collect::<Vec<_>>(),
490            [(0, 0), (0, 20), (1, 0), (2, 0), (4, 0), (5, 0), (6, 0), (7, 0), (7, 16), (8, 0)]
491                .map(|(slot, offset)| (U256::from(slot), offset))
492        );
493        for ty in layout.types.values() {
494            for id in [&ty.key, &ty.value].into_iter().flatten() {
495                assert!(layout.types.contains_key(id), "missing {id}");
496            }
497            if let Some(base) = ty.other.get("base") {
498                assert!(layout.types.contains_key(base.as_str().unwrap()));
499            }
500            if let Some(members) = ty.other.get("members") {
501                for member in members.as_array().unwrap() {
502                    assert!(layout.types.contains_key(member["type"].as_str().unwrap()));
503                    assert_eq!(member["slot"], "0");
504                }
505            }
506        }
507        assert!(layout.types.keys().all(|id| id.starts_with("erc7201(example.base)::")));
508    }
509
510    #[test]
511    fn annotation_errors_and_non_namespaced_contracts() {
512        for annotation in ["erc7201:", "erc7201:two words", "erc7201"] {
513            assert!(layouts(&format!("contract C {{\n/// @custom:storage-location {annotation}\nstruct Data {{ uint256 x; }}\n}}"), "C").is_err());
514        }
515        assert!(
516            layouts(
517                r#"contract C {
518            /// @custom:storage-location erc7201:first
519            /// @custom:storage-location erc7201:second
520            struct Data { uint256 x; }
521        }"#,
522                "C"
523            )
524            .is_err()
525        );
526        assert!(
527            layouts(
528                r#"contract C {
529            /// @custom:storage-location other:example
530            struct Data { uint256 x; }
531            uint256 value;
532        }"#,
533                "C"
534            )
535            .unwrap()
536            .is_empty()
537        );
538        // Duplicate declarations are retained for the merge's collision policy.
539        assert_eq!(
540            layouts(
541                r#"contract C {
542            /// @custom:storage-location erc7201:same
543            struct A { uint256 x; }
544            /// @custom:storage-location erc7201:same
545            struct B { uint256 y; }
546        }"#,
547                "C"
548            )
549            .unwrap()
550            .len(),
551            2
552        );
553    }
554
555    #[test]
556    fn annotations() {
557        assert_eq!(parse_namespace(" erc7201:example.main \n").unwrap(), Some("example.main"));
558        assert_eq!(parse_namespace("erc9999:example.main").unwrap(), None);
559        for invalid in ["erc7201", "erc7201:", "erc7201:two words"] {
560            assert!(parse_namespace(invalid).is_err());
561        }
562    }
563
564    #[test]
565    fn duplicate_namespaces_leave_layout_unchanged() {
566        let namespace = |id: &str, root: u64| StorageNamespace {
567            id: id.into(),
568            declaration: format!("test.sol:C.{id}"),
569            root: U256::from(root),
570            layout: serde_json::from_value(serde_json::json!({
571                "storage": [{
572                    "astId": 1, "contract": "test.sol:C", "label": "x", "offset": 0,
573                    "slot": root.to_string(), "type": format!("erc7201({id})::t_uint256")
574                }],
575                "types": {format!("erc7201({id})::t_uint256"): {
576                    "encoding": "inplace", "label": "uint256", "numberOfBytes": "32"
577                }}
578            }))
579            .unwrap(),
580        };
581        let mut layout = StorageLayout::default();
582        assert_eq!(
583            merge_storage_namespaces(
584                &mut layout,
585                vec![namespace("a", 1), namespace("b", 2), namespace("c", 1)]
586            )
587            .unwrap_err()
588            .to_string(),
589            "Duplicate ERC-7201 namespace `c` in `test.sol:C.a` and `test.sol:C.c`"
590        );
591        assert_eq!(layout, StorageLayout::default());
592
593        merge_storage_namespaces(&mut layout, vec![namespace("a", 1), namespace("b", 2)]).unwrap();
594        assert_eq!(
595            layout.storage.iter().map(|s| s.label.as_str()).collect::<Vec<_>>(),
596            ["a.x", "b.x"]
597        );
598        assert_eq!(layout.types.len(), 2);
599    }
600
601    #[test]
602    fn artifact_layouts() {
603        let mut parser = parser_at(
604            r#"contract C {
605            /// @custom:storage-location erc7201:example
606            struct Data { uint256 value; }
607        }
608        contract Plain { uint256 value; }"#,
609            "/project/src/test.sol",
610        );
611        let id = |name: &str| ArtifactId {
612            path: PathBuf::new(),
613            name: name.to_owned(),
614            source: PathBuf::from("src/test.sol"),
615            version: semver::Version::new(0, 8, 30),
616            build_id: String::new(),
617            profile: String::new(),
618        };
619        let layouts = extend_storage_layouts(
620            &mut parser,
621            Path::new("/project"),
622            [(id("C"), StorageLayout::default()), (id("Plain"), StorageLayout::default())],
623        );
624        assert_eq!(
625            layouts
626                .iter()
627                .map(|(id, layout)| (
628                    id.name.as_str(),
629                    layout.storage.iter().map(|s| s.label.as_str()).collect::<Vec<_>>()
630                ))
631                .collect::<Vec<_>>(),
632            [("C", vec!["example.value"]), ("Plain", vec![])]
633        );
634        assert_eq!(layouts[0].1.storage[0].contract, "src/test.sol:C");
635    }
636
637    #[test]
638    fn standard_json_sources() {
639        let sources = Sources::from_iter([
640            (
641                PathBuf::from("src/Token.sol"),
642                foundry_compilers::artifacts::Source::new(
643                    r#"import "@oz/Base.sol";
644                    contract Token is Base {}"#,
645                ),
646            ),
647            (
648                PathBuf::from("lib/oz/Base.sol"),
649                foundry_compilers::artifacts::Source::new(
650                    r#"contract Base {
651                    /// @custom:storage-location erc7201:openzeppelin.storage.ERC20
652                    struct ERC20Storage { mapping(address => uint256) balances; uint256 totalSupply; }
653                }"#,
654                ),
655            ),
656        ]);
657        let remappings = ["@oz/=lib/oz/".parse::<Remapping>().unwrap()];
658        let mut compiler = parse_standard_json(&sources, &remappings).unwrap();
659        let mut layout = StorageLayout::default();
660        compiler
661            .enter_mut(|compiler| {
662                lower(compiler)?;
663                let gcx = compiler.gcx();
664                let contract =
665                    find_contract(gcx, Path::new(""), Path::new("src/Token.sol"), "Token").unwrap();
666                extend_storage_layout(&mut layout, gcx, Path::new(""), contract)
667            })
668            .unwrap();
669        assert_eq!(
670            layout
671                .storage
672                .iter()
673                .map(|s| (s.label.as_str(), s.contract.as_str()))
674                .collect::<Vec<_>>(),
675            [
676                ("openzeppelin.storage.ERC20.balances", "lib/oz/Base.sol:Base"),
677                ("openzeppelin.storage.ERC20.totalSupply", "lib/oz/Base.sol:Base"),
678            ]
679        );
680
681        let plain = Sources::from_iter([(
682            PathBuf::from("src/Plain.sol"),
683            foundry_compilers::artifacts::Source::new("contract Plain { uint256 x; }"),
684        )]);
685        assert!(parse_standard_json(&plain, &[]).is_none());
686    }
687}