forge/cmd/
eip712.rs

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