forge/cmd/
eip712.rs

1use clap::{Parser, ValueHint};
2use eyre::Result;
3use foundry_cli::opts::{solar_pcx_from_build_opts, BuildOpts};
4use solar_parse::interface::Session;
5use solar_sema::{
6    hir::StructId,
7    thread_local::ThreadLocal,
8    ty::{Ty, TyKind},
9    GcxWrapper, Hir,
10};
11use std::{collections::BTreeMap, fmt::Write, path::PathBuf};
12
13foundry_config::impl_figment_convert!(Eip712Args, build);
14
15/// CLI arguments for `forge eip712`.
16#[derive(Clone, Debug, Parser)]
17pub struct Eip712Args {
18    /// The path to the file from which to read struct definitions.
19    #[arg(value_hint = ValueHint::FilePath, value_name = "PATH")]
20    pub target_path: PathBuf,
21
22    #[command(flatten)]
23    build: BuildOpts,
24}
25
26impl Eip712Args {
27    pub fn run(self) -> Result<()> {
28        let mut sess = Session::builder().with_stderr_emitter().build();
29        sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
30
31        let result = sess.enter(|| -> Result<()> {
32            // Set up the parsing context with the project paths and sources.
33            let parsing_context =
34                solar_pcx_from_build_opts(&sess, self.build, Some(vec![self.target_path]))?;
35
36            // Parse and resolve
37            let hir_arena = ThreadLocal::new();
38            if let Ok(Some(gcx)) = parsing_context.parse_and_lower(&hir_arena) {
39                let resolver = Resolver::new(gcx);
40                for id in &resolver.struct_ids() {
41                    if let Some(resolved) = resolver.resolve_struct_eip712(*id) {
42                        _ = sh_println!("{resolved}\n");
43                    }
44                }
45            }
46
47            Ok(())
48        });
49
50        eyre::ensure!(result.is_ok() && sess.dcx.has_errors().is_ok(), "failed parsing");
51
52        Ok(())
53    }
54}
55
56/// Generates the EIP-712 `encodeType` string for a given struct.
57///
58/// Requires a reference to the source HIR.
59pub struct Resolver<'hir> {
60    hir: &'hir Hir<'hir>,
61    gcx: GcxWrapper<'hir>,
62}
63
64impl<'hir> Resolver<'hir> {
65    /// Constructs a new [`Resolver`] for the supplied [`Hir`] instance.
66    pub fn new(gcx: GcxWrapper<'hir>) -> Self {
67        Self { hir: &gcx.get().hir, gcx }
68    }
69
70    /// Returns the [`StructId`]s of every user-defined struct in source order.
71    pub fn struct_ids(&self) -> Vec<StructId> {
72        self.hir.strukt_ids().collect()
73    }
74
75    /// Converts a given struct into its EIP-712 `encodeType` representation.
76    ///
77    /// Returns `None` if the struct, or any of its fields, contains constructs
78    /// not supported by EIP-712 (mappings, function types, errors, etc).
79    pub fn resolve_struct_eip712(&self, id: StructId) -> Option<String> {
80        let mut subtypes = BTreeMap::new();
81        subtypes.insert(self.hir.strukt(id).name.as_str().into(), id);
82        self.resolve_eip712_inner(id, &mut subtypes, true, None)
83    }
84
85    fn resolve_eip712_inner(
86        &self,
87        id: StructId,
88        subtypes: &mut BTreeMap<String, StructId>,
89        append_subtypes: bool,
90        rename: Option<&str>,
91    ) -> Option<String> {
92        let def = self.hir.strukt(id);
93        let mut result = format!("{}(", rename.unwrap_or(def.name.as_str()));
94
95        for (idx, field_id) in def.fields.iter().enumerate() {
96            let field = self.hir.variable(*field_id);
97            let ty = self.resolve_type(self.gcx.get().type_of_hir_ty(&field.ty), subtypes)?;
98
99            write!(result, "{ty} {name}", name = field.name?.as_str()).ok()?;
100
101            if idx < def.fields.len() - 1 {
102                result.push(',');
103            }
104        }
105
106        result.push(')');
107
108        if append_subtypes {
109            for (subtype_name, subtype_id) in
110                subtypes.iter().map(|(name, id)| (name.clone(), *id)).collect::<Vec<_>>()
111            {
112                if subtype_id == id {
113                    continue
114                }
115                let encoded_subtype =
116                    self.resolve_eip712_inner(subtype_id, subtypes, false, Some(&subtype_name))?;
117
118                result.push_str(&encoded_subtype);
119            }
120        }
121
122        Some(result)
123    }
124
125    fn resolve_type(
126        &self,
127        ty: Ty<'hir>,
128        subtypes: &mut BTreeMap<String, StructId>,
129    ) -> Option<String> {
130        let ty = ty.peel_refs();
131        match ty.kind {
132            TyKind::Elementary(elem_ty) => Some(elem_ty.to_abi_str().to_string()),
133            TyKind::Array(element_ty, size) => {
134                let inner_type = self.resolve_type(element_ty, subtypes)?;
135                let size = size.to_string();
136                Some(format!("{inner_type}[{size}]"))
137            }
138            TyKind::DynArray(element_ty) => {
139                let inner_type = self.resolve_type(element_ty, subtypes)?;
140                Some(format!("{inner_type}[]"))
141            }
142            TyKind::Udvt(ty, _) => self.resolve_type(ty, subtypes),
143            TyKind::Struct(id) => {
144                let def = self.hir.strukt(id);
145                let name = match subtypes.iter().find(|(_, cached_id)| id == **cached_id) {
146                    Some((name, _)) => name.to_string(),
147                    None => {
148                        // Otherwise, assign new name
149                        let mut i = 0;
150                        let mut name = def.name.as_str().into();
151                        while subtypes.contains_key(&name) {
152                            i += 1;
153                            name = format!("{}_{i}", def.name.as_str());
154                        }
155
156                        subtypes.insert(name.clone(), id);
157
158                        // Recursively resolve fields to populate subtypes
159                        for field_id in def.fields {
160                            let field_ty =
161                                self.gcx.get().type_of_hir_ty(&self.hir.variable(*field_id).ty);
162                            self.resolve_type(field_ty, subtypes)?;
163                        }
164                        name
165                    }
166                };
167
168                Some(name)
169            }
170            // For now, map enums to `uint8`
171            TyKind::Enum(_) => Some("uint8".to_string()),
172            // For now, map contracts to `address`
173            TyKind::Contract(_) => Some("address".to_string()),
174            // EIP-712 doesn't support tuples (should use structs), functions, mappings, nor errors
175            _ => None,
176        }
177    }
178}