forge/cmd/
selectors.rs

1use alloy_primitives::hex;
2use clap::Parser;
3use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Table};
4use eyre::Result;
5use foundry_cli::{
6    opts::{BuildOpts, CompilerOpts, ProjectPathOpts},
7    utils::{cache_local_signatures, FoundryPathExt},
8};
9use foundry_common::{
10    compile::{compile_target, PathOrContractInfo, ProjectCompiler},
11    selectors::{import_selectors, SelectorImportData},
12};
13use foundry_compilers::{artifacts::output_selection::ContractOutputSelection, info::ContractInfo};
14use foundry_config::Config;
15use std::fs::canonicalize;
16
17/// CLI arguments for `forge selectors`.
18#[derive(Clone, Debug, Parser)]
19pub enum SelectorsSubcommands {
20    /// Check for selector collisions between contracts
21    #[command(visible_alias = "co")]
22    Collision {
23        /// The first of the two contracts for which to look selector collisions for, in the form
24        /// `(<path>:)?<contractname>`.
25        first_contract: ContractInfo,
26
27        /// The second of the two contracts for which to look selector collisions for, in the form
28        /// `(<path>:)?<contractname>`.
29        second_contract: ContractInfo,
30
31        #[command(flatten)]
32        build: Box<BuildOpts>,
33    },
34
35    /// Upload selectors to registry
36    #[command(visible_alias = "up")]
37    Upload {
38        /// The name of the contract to upload selectors for.
39        /// Can also be in form of `path:contract name`.
40        #[arg(required_unless_present = "all")]
41        contract: Option<PathOrContractInfo>,
42
43        /// Upload selectors for all contracts in the project.
44        #[arg(long, required_unless_present = "contract")]
45        all: bool,
46
47        #[command(flatten)]
48        project_paths: ProjectPathOpts,
49    },
50
51    /// List selectors from current workspace
52    #[command(visible_alias = "ls")]
53    List {
54        /// The name of the contract to list selectors for.
55        #[arg(help = "The name of the contract to list selectors for.")]
56        contract: Option<String>,
57
58        #[command(flatten)]
59        project_paths: ProjectPathOpts,
60    },
61
62    /// Find if a selector is present in the project
63    #[command(visible_alias = "f")]
64    Find {
65        /// The selector to search for
66        #[arg(help = "The selector to search for (with or without 0x prefix)")]
67        selector: String,
68
69        #[command(flatten)]
70        project_paths: ProjectPathOpts,
71    },
72
73    /// Cache project selectors (enables trace with local contracts functions and events).
74    #[command(visible_alias = "c")]
75    Cache {
76        #[command(flatten)]
77        project_paths: ProjectPathOpts,
78    },
79}
80
81impl SelectorsSubcommands {
82    pub async fn run(self) -> Result<()> {
83        match self {
84            Self::Cache { project_paths } => {
85                sh_println!("Caching selectors for contracts in the project...")?;
86                let build_args = BuildOpts {
87                    project_paths,
88                    compiler: CompilerOpts {
89                        extra_output: vec![ContractOutputSelection::Abi],
90                        ..Default::default()
91                    },
92                    ..Default::default()
93                };
94
95                // compile the project to get the artifacts/abis
96                let project = build_args.project()?;
97                let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
98                cache_local_signatures(&outcome, Config::foundry_cache_dir().unwrap())?
99            }
100            Self::Upload { contract, all, project_paths } => {
101                let build_args = BuildOpts {
102                    project_paths: project_paths.clone(),
103                    compiler: CompilerOpts {
104                        extra_output: vec![ContractOutputSelection::Abi],
105                        ..Default::default()
106                    },
107                    ..Default::default()
108                };
109
110                let project = build_args.project()?;
111                let output = if let Some(contract_info) = &contract {
112                    let Some(contract_name) = contract_info.name() else {
113                        eyre::bail!("No contract name provided.")
114                    };
115
116                    let target_path = contract_info
117                        .path()
118                        .map(Ok)
119                        .unwrap_or_else(|| project.find_contract_path(contract_name))?;
120                    compile_target(&target_path, &project, false)?
121                } else {
122                    ProjectCompiler::new().compile(&project)?
123                };
124                let artifacts = if all {
125                    output
126                        .into_artifacts_with_files()
127                        .filter(|(file, _, _)| {
128                            let is_sources_path = file.starts_with(&project.paths.sources);
129                            let is_test = file.is_sol_test();
130
131                            is_sources_path && !is_test
132                        })
133                        .map(|(_, contract, artifact)| (contract, artifact))
134                        .collect()
135                } else {
136                    let contract_info = contract.unwrap();
137                    let contract = contract_info.name().unwrap().to_string();
138
139                    let found_artifact = if let Some(path) = contract_info.path() {
140                        output.find(project.root().join(path).as_path(), &contract)
141                    } else {
142                        output.find_first(&contract)
143                    };
144
145                    let artifact = found_artifact
146                        .ok_or_else(|| {
147                            eyre::eyre!(
148                                "Could not find artifact `{contract}` in the compiled artifacts"
149                            )
150                        })?
151                        .clone();
152                    vec![(contract, artifact)]
153                };
154
155                let mut artifacts = artifacts.into_iter().peekable();
156                while let Some((contract, artifact)) = artifacts.next() {
157                    let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
158                    if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
159                        continue
160                    }
161
162                    sh_println!("Uploading selectors for {contract}...")?;
163
164                    // upload abi to selector database
165                    import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe();
166
167                    if artifacts.peek().is_some() {
168                        sh_println!()?
169                    }
170                }
171            }
172            Self::Collision { mut first_contract, mut second_contract, build } => {
173                // Compile the project with the two contracts included
174                let project = build.project()?;
175                let mut compiler = ProjectCompiler::new().quiet(true);
176
177                if let Some(contract_path) = &mut first_contract.path {
178                    let target_path = canonicalize(&*contract_path)?;
179                    *contract_path = target_path.to_string_lossy().to_string();
180                    compiler = compiler.files([target_path]);
181                }
182                if let Some(contract_path) = &mut second_contract.path {
183                    let target_path = canonicalize(&*contract_path)?;
184                    *contract_path = target_path.to_string_lossy().to_string();
185                    compiler = compiler.files([target_path]);
186                }
187
188                let output = compiler.compile(&project)?;
189
190                // Check method selectors for collisions
191                let methods = |contract: &ContractInfo| -> eyre::Result<_> {
192                    let artifact = output
193                        .find_contract(contract)
194                        .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?;
195                    artifact.method_identifiers.as_ref().ok_or_else(|| {
196                        eyre::eyre!("Could not find method identifiers for {contract}")
197                    })
198                };
199                let first_method_map = methods(&first_contract)?;
200                let second_method_map = methods(&second_contract)?;
201
202                let colliding_methods: Vec<(&String, &String, &String)> = first_method_map
203                    .iter()
204                    .filter_map(|(k1, v1)| {
205                        second_method_map
206                            .iter()
207                            .find_map(|(k2, v2)| if **v2 == *v1 { Some((k2, v2)) } else { None })
208                            .map(|(k2, v2)| (v2, k1, k2))
209                    })
210                    .collect();
211
212                if colliding_methods.is_empty() {
213                    sh_println!("No colliding method selectors between the two contracts.")?;
214                } else {
215                    let mut table = Table::new();
216                    table.apply_modifier(UTF8_ROUND_CORNERS);
217                    table.set_header([
218                        String::from("Selector"),
219                        first_contract.name,
220                        second_contract.name,
221                    ]);
222                    for method in &colliding_methods {
223                        table.add_row([method.0, method.1, method.2]);
224                    }
225                    sh_println!("{} collisions found:", colliding_methods.len())?;
226                    sh_println!("\n{table}\n")?;
227                }
228            }
229            Self::List { contract, project_paths } => {
230                sh_println!("Listing selectors for contracts in the project...")?;
231                let build_args = BuildOpts {
232                    project_paths,
233                    compiler: CompilerOpts {
234                        extra_output: vec![ContractOutputSelection::Abi],
235                        ..Default::default()
236                    },
237                    ..Default::default()
238                };
239
240                // compile the project to get the artifacts/abis
241                let project = build_args.project()?;
242                let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
243                let artifacts = if let Some(contract) = contract {
244                    let found_artifact = outcome.find_first(&contract);
245                    let artifact = found_artifact
246                        .ok_or_else(|| {
247                            let candidates = outcome
248                                .artifacts()
249                                .map(|(name, _,)| name)
250                                .collect::<Vec<_>>();
251                            let suggestion = if let Some(suggestion) = foundry_cli::utils::did_you_mean(&contract, candidates).pop() {
252                                format!("\nDid you mean `{suggestion}`?")
253                            } else {
254                                String::new()
255                            };
256                            eyre::eyre!(
257                                "Could not find artifact `{contract}` in the compiled artifacts{suggestion}",
258                            )
259                        })?
260                        .clone();
261                    vec![(contract, artifact)]
262                } else {
263                    outcome
264                        .into_artifacts_with_files()
265                        .filter(|(file, _, _)| {
266                            let is_sources_path = file.starts_with(&project.paths.sources);
267                            let is_test = file.is_sol_test();
268
269                            is_sources_path && !is_test
270                        })
271                        .map(|(_, contract, artifact)| (contract, artifact))
272                        .collect()
273                };
274
275                let mut artifacts = artifacts.into_iter().peekable();
276
277                while let Some((contract, artifact)) = artifacts.next() {
278                    let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
279                    if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
280                        continue
281                    }
282
283                    sh_println!("{contract}")?;
284
285                    let mut table = Table::new();
286                    table.apply_modifier(UTF8_ROUND_CORNERS);
287
288                    table.set_header(["Type", "Signature", "Selector"]);
289
290                    for func in abi.functions() {
291                        let sig = func.signature();
292                        let selector = func.selector();
293                        table.add_row(["Function", &sig, &hex::encode_prefixed(selector)]);
294                    }
295
296                    for event in abi.events() {
297                        let sig = event.signature();
298                        let selector = event.selector();
299                        table.add_row(["Event", &sig, &hex::encode_prefixed(selector)]);
300                    }
301
302                    for error in abi.errors() {
303                        let sig = error.signature();
304                        let selector = error.selector();
305                        table.add_row(["Error", &sig, &hex::encode_prefixed(selector)]);
306                    }
307
308                    sh_println!("\n{table}\n")?;
309
310                    if artifacts.peek().is_some() {
311                        sh_println!()?
312                    }
313                }
314            }
315
316            Self::Find { selector, project_paths } => {
317                sh_println!("Searching for selector {selector:?} in the project...")?;
318
319                let build_args = BuildOpts {
320                    project_paths,
321                    compiler: CompilerOpts {
322                        extra_output: vec![ContractOutputSelection::Abi],
323                        ..Default::default()
324                    },
325                    ..Default::default()
326                };
327
328                let project = build_args.project()?;
329                let outcome = ProjectCompiler::new().quiet(true).compile(&project)?;
330                let artifacts = outcome
331                    .into_artifacts_with_files()
332                    .filter(|(file, _, _)| {
333                        let is_sources_path = file.starts_with(&project.paths.sources);
334                        let is_test = file.is_sol_test();
335                        is_sources_path && !is_test
336                    })
337                    .collect::<Vec<_>>();
338
339                let mut table = Table::new();
340                table.apply_modifier(UTF8_ROUND_CORNERS);
341
342                table.set_header(["Type", "Signature", "Selector", "Contract"]);
343
344                for (_file, contract, artifact) in artifacts {
345                    let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
346
347                    let selector_bytes =
348                        hex::decode(selector.strip_prefix("0x").unwrap_or(&selector))?;
349
350                    for func in abi.functions() {
351                        if func.selector().as_slice().starts_with(selector_bytes.as_slice()) {
352                            table.add_row([
353                                "Function",
354                                &func.signature(),
355                                &hex::encode_prefixed(func.selector()),
356                                contract.as_str(),
357                            ]);
358                        }
359                    }
360
361                    for event in abi.events() {
362                        if event.selector().as_slice().starts_with(selector_bytes.as_slice()) {
363                            table.add_row([
364                                "Event",
365                                &event.signature(),
366                                &hex::encode_prefixed(event.selector()),
367                                contract.as_str(),
368                            ]);
369                        }
370                    }
371
372                    for error in abi.errors() {
373                        if error.selector().as_slice().starts_with(selector_bytes.as_slice()) {
374                            table.add_row([
375                                "Error",
376                                &error.signature(),
377                                &hex::encode_prefixed(error.selector()),
378                                contract.as_str(),
379                            ]);
380                        }
381                    }
382                }
383
384                if table.row_count() > 0 {
385                    sh_println!("\nFound {} instance(s)...", table.row_count())?;
386                    sh_println!("\n{table}\n")?;
387                } else {
388                    return Err(eyre::eyre!("\nSelector not found in the project."));
389                }
390            }
391        }
392        Ok(())
393    }
394}