Skip to main content

forge/cmd/
selectors.rs

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