1use 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
31pub 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
47pub 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
82pub fn extend_storage_layout(
87 layout: &mut StorageLayout,
88 gcx: Gcx<'_>,
89 root: &Path,
90 contract: hir::ContractId,
91) -> Result<()> {
92 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
101pub 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
115pub 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
152pub 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
163const ANNOTATION: &str = "erc7201";
165
166#[derive(Clone, Debug)]
168struct StorageNamespace {
169 id: String,
171 declaration: String,
173 root: U256,
175 layout: StorageLayout,
180}
181
182struct 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
203pub fn has_annotations(compiler: &Compiler) -> bool {
205 compiler.sess().source_map().files().iter().any(|file| has_annotation(&file.src))
206}
207
208fn has_annotation(source: &str) -> bool {
210 source.contains(ANNOTATION)
211}
212
213fn 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
228fn 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 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
268fn 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
301fn 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
325fn 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 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 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}