Skip to main content

forge/cmd/
eip712.rs

1use alloy_primitives::{B256, keccak256};
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::opts::{BuildOpts, solar_pcx_from_build_opts};
5use serde::Serialize;
6use solar_parse::interface::Session;
7use solar_sema::{
8    GcxWrapper, Hir,
9    hir::StructId,
10    thread_local::ThreadLocal,
11    ty::{Ty, TyKind},
12};
13use std::{
14    collections::BTreeMap,
15    fmt::{Display, Formatter, Result as FmtResult, Write},
16    path::{Path, PathBuf},
17    slice,
18};
19
20foundry_config::impl_figment_convert!(Eip712Args, build);
21
22/// CLI arguments for `forge eip712`.
23#[derive(Clone, Debug, Parser)]
24pub struct Eip712Args {
25    /// The path to the file from which to read struct definitions.
26    #[arg(value_hint = ValueHint::FilePath, value_name = "PATH")]
27    pub target_path: PathBuf,
28
29    /// Output in JSON format.
30    #[arg(long, help = "Output in JSON format")]
31    pub json: bool,
32
33    #[command(flatten)]
34    build: BuildOpts,
35}
36
37#[derive(Debug, Serialize)]
38struct Eip712Output {
39    path: String,
40    #[serde(rename = "type")]
41    ty: String,
42    hash: B256,
43}
44
45impl Display for Eip712Output {
46    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
47        writeln!(f, "{}:", self.path)?;
48        writeln!(f, " - type: {}", self.ty)?;
49        writeln!(f, " - hash: {}", self.hash)
50    }
51}
52
53impl Eip712Args {
54    pub fn run(self) -> Result<()> {
55        let mut sess = Session::builder().with_stderr_emitter().build();
56        sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
57
58        sess.enter_parallel(|| -> Result<()> {
59            // Set up the parsing context with the project paths and sources.
60            let parsing_context = solar_pcx_from_build_opts(
61                &sess,
62                &self.build,
63                None,
64                Some(slice::from_ref(&self.target_path)),
65            )?;
66
67            // Parse and resolve
68            let hir_arena = ThreadLocal::new();
69            let Ok(Some(gcx)) = parsing_context.parse_and_lower(&hir_arena) else {
70                return Err(eyre::eyre!("failed parsing"));
71            };
72            let resolver = Resolver::new(gcx);
73
74            let outputs = resolver
75                .struct_ids()
76                .filter_map(|id| {
77                    let resolved = resolver.resolve_struct_eip712(id)?;
78                    Some(Eip712Output {
79                        path: resolver.get_struct_path(id),
80                        hash: keccak256(resolved.as_bytes()),
81                        ty: resolved,
82                    })
83                })
84                .collect::<Vec<_>>();
85
86            if self.json {
87                sh_println!("{json}", json = serde_json::to_string_pretty(&outputs)?)?;
88            } else {
89                for output in &outputs {
90                    sh_println!("{output}")?;
91                }
92            }
93
94            Ok(())
95        })?;
96
97        eyre::ensure!(sess.dcx.has_errors().is_ok(), "errors occurred");
98
99        Ok(())
100    }
101}
102
103/// Generates the EIP-712 `encodeType` string for a given struct.
104///
105/// Requires a reference to the source HIR.
106pub struct Resolver<'hir> {
107    gcx: GcxWrapper<'hir>,
108}
109
110impl<'hir> Resolver<'hir> {
111    /// Constructs a new [`Resolver`] for the supplied [`Hir`] instance.
112    pub fn new(gcx: GcxWrapper<'hir>) -> Self {
113        Self { gcx }
114    }
115
116    #[inline]
117    fn hir(&self) -> &'hir Hir<'hir> {
118        &self.gcx.get().hir
119    }
120
121    /// Returns the [`StructId`]s of every user-defined struct in source order.
122    pub fn struct_ids(&self) -> impl Iterator<Item = StructId> {
123        self.hir().strukt_ids()
124    }
125
126    /// Returns the path for a struct, with the format: `file.sol > MyContract > MyStruct`
127    pub fn get_struct_path(&self, id: StructId) -> String {
128        let strukt = self.hir().strukt(id).name.as_str();
129        match self.hir().strukt(id).contract {
130            Some(cid) => {
131                let full_name = self.gcx.get().contract_fully_qualified_name(cid).to_string();
132                let relevant = Path::new(&full_name)
133                    .file_name()
134                    .and_then(|s| s.to_str())
135                    .unwrap_or(&full_name);
136
137                if let Some((file, contract)) = relevant.rsplit_once(':') {
138                    format!("{file} > {contract} > {strukt}")
139                } else {
140                    format!("{relevant} > {strukt}")
141                }
142            }
143            None => strukt.to_string(),
144        }
145    }
146
147    /// Converts a given struct into its EIP-712 `encodeType` representation.
148    ///
149    /// Returns `None` if the struct, or any of its fields, contains constructs
150    /// not supported by EIP-712 (mappings, function types, errors, etc).
151    pub fn resolve_struct_eip712(&self, id: StructId) -> Option<String> {
152        let mut subtypes = BTreeMap::new();
153        subtypes.insert(self.hir().strukt(id).name.as_str().into(), id);
154        self.resolve_eip712_inner(id, &mut subtypes, true, None)
155    }
156
157    fn resolve_eip712_inner(
158        &self,
159        id: StructId,
160        subtypes: &mut BTreeMap<String, StructId>,
161        append_subtypes: bool,
162        rename: Option<&str>,
163    ) -> Option<String> {
164        let def = self.hir().strukt(id);
165        let mut result = format!("{}(", rename.unwrap_or(def.name.as_str()));
166
167        for (idx, field_id) in def.fields.iter().enumerate() {
168            let field = self.hir().variable(*field_id);
169            let ty = self.resolve_type(self.gcx.get().type_of_hir_ty(&field.ty), subtypes)?;
170
171            write!(result, "{ty} {name}", name = field.name?.as_str()).ok()?;
172
173            if idx < def.fields.len() - 1 {
174                result.push(',');
175            }
176        }
177
178        result.push(')');
179
180        if append_subtypes {
181            for (subtype_name, subtype_id) in
182                subtypes.iter().map(|(name, id)| (name.clone(), *id)).collect::<Vec<_>>()
183            {
184                if subtype_id == id {
185                    continue;
186                }
187                let encoded_subtype =
188                    self.resolve_eip712_inner(subtype_id, subtypes, false, Some(&subtype_name))?;
189
190                result.push_str(&encoded_subtype);
191            }
192        }
193
194        Some(result)
195    }
196
197    fn resolve_type(
198        &self,
199        ty: Ty<'hir>,
200        subtypes: &mut BTreeMap<String, StructId>,
201    ) -> Option<String> {
202        let ty = ty.peel_refs();
203        match ty.kind {
204            TyKind::Elementary(elem_ty) => Some(elem_ty.to_abi_str().to_string()),
205            TyKind::Array(element_ty, size) => {
206                let inner_type = self.resolve_type(element_ty, subtypes)?;
207                let size = size.to_string();
208                Some(format!("{inner_type}[{size}]"))
209            }
210            TyKind::DynArray(element_ty) => {
211                let inner_type = self.resolve_type(element_ty, subtypes)?;
212                Some(format!("{inner_type}[]"))
213            }
214            TyKind::Udvt(ty, _) => self.resolve_type(ty, subtypes),
215            TyKind::Struct(id) => {
216                let def = self.hir().strukt(id);
217                let name = match subtypes.iter().find(|(_, cached_id)| id == **cached_id) {
218                    Some((name, _)) => name.to_string(),
219                    None => {
220                        // Otherwise, assign new name
221                        let mut i = 0;
222                        let mut name = def.name.as_str().into();
223                        while subtypes.contains_key(&name) {
224                            i += 1;
225                            name = format!("{}_{i}", def.name.as_str());
226                        }
227
228                        subtypes.insert(name.clone(), id);
229
230                        // Recursively resolve fields to populate subtypes
231                        for &field_id in def.fields {
232                            let field_ty = self.gcx.get().type_of_item(field_id.into());
233                            self.resolve_type(field_ty, subtypes)?;
234                        }
235                        name
236                    }
237                };
238
239                Some(name)
240            }
241            // For now, map enums to `uint8`
242            TyKind::Enum(_) => Some("uint8".to_string()),
243            // For now, map contracts to `address`
244            TyKind::Contract(_) => Some("address".to_string()),
245            // EIP-712 doesn't support tuples (should use structs), functions, mappings, nor errors
246            _ => None,
247        }
248    }
249}