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, utils::LoadConfig};
5use foundry_common::{compile::ProjectCompiler, shell};
6use serde::Serialize;
7use solar::sema::{
8    Gcx, Hir,
9    hir::StructId,
10    ty::{Ty, TyKind},
11};
12use std::{
13    collections::BTreeMap,
14    fmt::{Display, Formatter, Result as FmtResult, Write},
15    ops::ControlFlow,
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    #[command(flatten)]
29    build: BuildOpts,
30}
31
32#[derive(Debug, Serialize)]
33struct Eip712Output {
34    path: String,
35    #[serde(rename = "type")]
36    ty: String,
37    hash: B256,
38}
39
40impl Display for Eip712Output {
41    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
42        writeln!(f, "{}:", self.path)?;
43        writeln!(f, " - type: {}", self.ty)?;
44        writeln!(f, " - hash: {}", self.hash)
45    }
46}
47
48impl Eip712Args {
49    pub fn run(self) -> Result<()> {
50        let config = self.build.load_config_with_dependencies()?;
51        let project = config.solar_project()?;
52        let mut output = ProjectCompiler::new().files([self.target_path]).compile(&project)?;
53        let compiler = output.parser_mut().solc_mut().compiler_mut();
54        // Solc validates the source version; Solar only provides type information.
55        compiler.dcx().set_allowed_diagnostic_codes(["5333".into()]);
56        compiler.enter_mut(|compiler| -> Result<()> {
57            let Ok(ControlFlow::Continue(())) = compiler.lower_asts() else { return Ok(()) };
58            let gcx = compiler.gcx();
59            let resolver = Resolver::new(gcx);
60
61            let outputs = resolver
62                .struct_ids()
63                .filter_map(|id| {
64                    let resolved = resolver.resolve_struct_eip712(id)?;
65                    Some(Eip712Output {
66                        path: resolver.get_struct_path(id),
67                        hash: keccak256(resolved.as_bytes()),
68                        ty: resolved,
69                    })
70                })
71                .collect::<Vec<_>>();
72
73            if shell::is_json() {
74                sh_println!("{json}", json = serde_json::to_string_pretty(&outputs)?)?;
75            } else {
76                for output in &outputs {
77                    sh_eprintln!("{output}")?;
78                }
79            }
80
81            Ok(())
82        })?;
83
84        // `compiler.sess()` inside of `ProjectCompileOutput` is built with `with_buffer_emitter`.
85        let diags = compiler.sess().dcx.emitted_diagnostics().unwrap();
86        if compiler.sess().dcx.has_errors().is_err() {
87            eyre::bail!("{diags}");
88        }
89        let _ = sh_eprint!("{diags}");
90
91        Ok(())
92    }
93}
94
95/// Generates the EIP-712 `encodeType` string for a given struct.
96///
97/// Requires a reference to the source HIR.
98pub struct Resolver<'gcx> {
99    gcx: Gcx<'gcx>,
100}
101
102impl<'gcx> Resolver<'gcx> {
103    /// Constructs a new [`Resolver`] for the supplied [`Hir`] instance.
104    pub const fn new(gcx: Gcx<'gcx>) -> Self {
105        Self { gcx }
106    }
107
108    #[inline]
109    fn hir(&self) -> &'gcx Hir<'gcx> {
110        &self.gcx.hir
111    }
112
113    /// Returns the [`StructId`]s of every user-defined struct in source order.
114    pub fn struct_ids(&self) -> impl Iterator<Item = StructId> {
115        self.hir().strukt_ids()
116    }
117
118    /// Returns the path for a struct, with the format: `file.sol > MyContract > MyStruct`
119    pub fn get_struct_path(&self, id: StructId) -> String {
120        let strukt = self.hir().strukt(id).name.as_str();
121        match self.hir().strukt(id).contract {
122            Some(cid) => {
123                let full_name = self.gcx.contract_fully_qualified_name(cid).to_string();
124                let relevant = Path::new(&full_name)
125                    .file_name()
126                    .and_then(|s| s.to_str())
127                    .unwrap_or(&full_name);
128
129                if let Some((file, contract)) = relevant.rsplit_once(':') {
130                    format!("{file} > {contract} > {strukt}")
131                } else {
132                    format!("{relevant} > {strukt}")
133                }
134            }
135            None => strukt.to_string(),
136        }
137    }
138
139    /// Converts a given struct into its EIP-712 `encodeType` representation.
140    ///
141    /// Returns `None` if the struct, or any of its fields, contains constructs
142    /// not supported by EIP-712 (mappings, function types, errors, etc).
143    pub fn resolve_struct_eip712(&self, id: StructId) -> Option<String> {
144        let mut subtypes = BTreeMap::new();
145        subtypes.insert(self.hir().strukt(id).name.as_str().into(), id);
146        self.resolve_eip712_inner(id, &mut subtypes, true, None)
147    }
148
149    fn resolve_eip712_inner(
150        &self,
151        id: StructId,
152        subtypes: &mut BTreeMap<String, StructId>,
153        append_subtypes: bool,
154        rename: Option<&str>,
155    ) -> Option<String> {
156        let def = self.hir().strukt(id);
157        let mut result = format!("{}(", rename.unwrap_or(def.name.as_str()));
158
159        for (idx, field_id) in def.fields.iter().enumerate() {
160            let field = self.hir().variable(*field_id);
161            let ty = self.resolve_type(self.gcx.type_of_hir_ty(&field.ty), subtypes)?;
162
163            write!(result, "{ty} {name}", name = field.name?.as_str()).ok()?;
164
165            if idx < def.fields.len() - 1 {
166                result.push(',');
167            }
168        }
169
170        result.push(')');
171
172        if append_subtypes {
173            for (subtype_name, subtype_id) in
174                subtypes.iter().map(|(name, id)| (name.clone(), *id)).collect::<Vec<_>>()
175            {
176                if subtype_id == id {
177                    continue;
178                }
179                let encoded_subtype =
180                    self.resolve_eip712_inner(subtype_id, subtypes, false, Some(&subtype_name))?;
181
182                result.push_str(&encoded_subtype);
183            }
184        }
185
186        Some(result)
187    }
188
189    fn resolve_type(
190        &self,
191        ty: Ty<'gcx>,
192        subtypes: &mut BTreeMap<String, StructId>,
193    ) -> Option<String> {
194        let ty = ty.peel_refs();
195        match ty.kind {
196            TyKind::Elementary(elem_ty) => Some(elem_ty.to_abi_str().to_string()),
197            TyKind::Array(element_ty, size) => {
198                let inner_type = self.resolve_type(element_ty, subtypes)?;
199                let size = size.to_string();
200                Some(format!("{inner_type}[{size}]"))
201            }
202            TyKind::DynArray(element_ty) => {
203                let inner_type = self.resolve_type(element_ty, subtypes)?;
204                Some(format!("{inner_type}[]"))
205            }
206            TyKind::Udvt(ty, _) => self.resolve_type(ty, subtypes),
207            TyKind::Struct(id) => {
208                let def = self.hir().strukt(id);
209                let name = match subtypes.iter().find(|(_, cached_id)| id == **cached_id) {
210                    Some((name, _)) => name.clone(),
211                    None => {
212                        // Otherwise, assign new name
213                        let mut i = 0;
214                        let mut name = def.name.as_str().into();
215                        while subtypes.contains_key(&name) {
216                            i += 1;
217                            name = format!("{}_{i}", def.name.as_str());
218                        }
219
220                        subtypes.insert(name.clone(), id);
221
222                        // Recursively resolve fields to populate subtypes
223                        for &field_id in def.fields {
224                            let field_ty = self.gcx.type_of_item(field_id.into());
225                            self.resolve_type(field_ty, subtypes)?;
226                        }
227                        name
228                    }
229                };
230
231                Some(name)
232            }
233            // For now, map enums to `uint8`
234            TyKind::Enum(_) => Some("uint8".to_string()),
235            // For now, map contracts to `address`
236            TyKind::Contract(_) => Some("address".to_string()),
237            // EIP-712 doesn't support tuples (should use structs), functions, mappings, nor errors
238            _ => None,
239        }
240    }
241}