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, compiler) = project_from_paths(project_paths)?;
102                let outcome = compile_abi_project(&mut project, compiler.quiet(true))?;
103                cache_local_signatures(&outcome)?;
104            }
105            Self::Upload { contract, all, project_paths } => {
106                let (mut project, compiler) = project_from_paths(project_paths)?;
107                let output = if let Some(contract_info) = &contract {
108                    let Some(contract_name) = contract_info.name() else {
109                        eyre::bail!("No contract name provided.");
110                    };
111
112                    let target_path = contract_info
113                        .path()
114                        .map(Ok)
115                        .unwrap_or_else(|| project.find_contract_path(contract_name))?;
116                    compile_abi_project(&mut project, compiler.files([target_path]))?
117                } else {
118                    compile_abi_project(&mut project, compiler)?
119                };
120                let artifacts = if all {
121                    output
122                        .into_artifacts_with_files()
123                        .filter(|(file, _, _)| {
124                            let is_sources_path = file.starts_with(&project.paths.sources);
125                            let is_test = file.is_sol_test();
126
127                            is_sources_path && !is_test
128                        })
129                        .map(|(_, contract, artifact)| (contract, artifact))
130                        .collect()
131                } else {
132                    let contract_info = contract.unwrap();
133                    let contract = contract_info.name().unwrap().to_string();
134
135                    let found_artifact = if let Some(path) = contract_info.path() {
136                        output.find(project.root().join(path).as_path(), &contract)
137                    } else {
138                        output.find_first(&contract)
139                    };
140
141                    let artifact = found_artifact
142                        .ok_or_else(|| {
143                            eyre::eyre!(
144                                "Could not find artifact `{contract}` in the compiled artifacts"
145                            )
146                        })?
147                        .clone();
148                    vec![(contract, artifact)]
149                };
150
151                let mut artifacts = artifacts.into_iter().peekable();
152                while let Some((contract, artifact)) = artifacts.next() {
153                    let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
154                    if abi.functions.is_empty() && abi.events.is_empty() && abi.errors.is_empty() {
155                        continue;
156                    }
157
158                    sh_status!("Uploading selectors for {contract}...")?;
159
160                    // upload abi to selector database
161                    import_selectors(SelectorImportData::Abi(vec![abi])).await?.describe();
162
163                    if artifacts.peek().is_some() {
164                        sh_println!()?
165                    }
166                }
167            }
168            Self::Collision { mut first_contract, mut second_contract, build } => {
169                // Compile the project with the two contracts included
170                let user_extra_output = !build.compiler.extra_output.is_empty()
171                    || !build.compiler.extra_output_files.is_empty();
172                let mut project = build.project()?;
173                if !user_extra_output && !project.build_info {
174                    project.no_artifacts = true;
175                    project.update_output_selection(|selection| {
176                        *selection = OutputSelection::common_output_selection([
177                            ContractOutputSelection::Evm(EvmOutputSelection::MethodIdentifiers)
178                                .to_string(),
179                        ]);
180                    });
181                }
182                let mut compiler = ProjectCompiler::new().quiet(true);
183
184                if let Some(contract_path) = &mut first_contract.path {
185                    let target_path = canonicalize(&*contract_path)?;
186                    *contract_path = target_path.to_string_lossy().to_string();
187                    compiler = compiler.files([target_path]);
188                }
189                if let Some(contract_path) = &mut second_contract.path {
190                    let target_path = canonicalize(&*contract_path)?;
191                    *contract_path = target_path.to_string_lossy().to_string();
192                    compiler = compiler.files([target_path]);
193                }
194
195                let output = compiler.compile(&project)?;
196
197                // Check method selectors for collisions
198                let methods = |contract: &ContractInfo| -> eyre::Result<_> {
199                    let artifact = output
200                        .find_contract(contract)
201                        .ok_or_else(|| eyre::eyre!("Could not find artifact for {contract}"))?;
202                    artifact.method_identifiers.as_ref().ok_or_else(|| {
203                        eyre::eyre!("Could not find method identifiers for {contract}")
204                    })
205                };
206                let first_method_map = methods(&first_contract)?;
207                let second_method_map = methods(&second_contract)?;
208
209                let colliding_methods: Vec<(&String, &String, &String)> = first_method_map
210                    .iter()
211                    .filter_map(|(k1, v1)| {
212                        second_method_map
213                            .iter()
214                            .find_map(|(k2, v2)| (**v2 == *v1).then_some((k2, v2)))
215                            .map(|(k2, v2)| (v2, k1, k2))
216                    })
217                    .collect();
218
219                if colliding_methods.is_empty() {
220                    sh_println!("No colliding method selectors between the two contracts.")?;
221                } else {
222                    let mut table = Table::new();
223                    if shell::is_markdown() {
224                        table.load_preset(ASCII_MARKDOWN);
225                    } else {
226                        table.apply_modifier(UTF8_ROUND_CORNERS);
227                    }
228                    table.set_header([
229                        String::from("Selector"),
230                        first_contract.name,
231                        second_contract.name,
232                    ]);
233                    for method in &colliding_methods {
234                        #[allow(clippy::tuple_array_conversions)]
235                        table.add_row(<[_; 3]>::from(*method));
236                    }
237                    sh_println!("{} collisions found:", colliding_methods.len())?;
238                    sh_println!("\n{table}\n")?;
239                }
240            }
241            Self::List { contract, project_paths, no_group } => {
242                sh_status!("Listing selectors for contracts in the project...")?;
243                let (mut project, compiler) = project_from_paths(project_paths)?;
244                let outcome = compile_abi_project(&mut project, compiler.quiet(true))?;
245                let artifacts = if let Some(contract) = contract {
246                    let found_artifact = outcome.find_first(&contract);
247                    let artifact = found_artifact
248                        .ok_or_else(|| {
249                            let candidates = outcome
250                                .artifacts()
251                                .map(|(name, _,)| name)
252                                .collect::<Vec<_>>();
253                            let suggestion = if let Some(suggestion) = foundry_cli::utils::did_you_mean(&contract, candidates).pop() {
254                                format!("\nDid you mean `{suggestion}`?")
255                            } else {
256                                String::new()
257                            };
258                            eyre::eyre!(
259                                "Could not find artifact `{contract}` in the compiled artifacts{suggestion}",
260                            )
261                        })?
262                        .clone();
263                    vec![(contract, artifact)]
264                } else {
265                    outcome
266                        .into_artifacts_with_files()
267                        .filter(|(file, _, _)| {
268                            let is_sources_path = file.starts_with(&project.paths.sources);
269                            let is_test = file.is_sol_test();
270
271                            is_sources_path && !is_test
272                        })
273                        .map(|(_, contract, artifact)| (contract, artifact))
274                        .collect()
275                };
276
277                let mut artifacts = artifacts.into_iter();
278
279                #[derive(PartialEq, PartialOrd, Eq, Ord)]
280                enum SelectorType {
281                    Function,
282                    Event,
283                    Error,
284                }
285                impl std::fmt::Display for SelectorType {
286                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287                        match self {
288                            Self::Function => write!(f, "Function"),
289                            Self::Event => write!(f, "Event"),
290                            Self::Error => write!(f, "Error"),
291                        }
292                    }
293                }
294
295                let mut selectors =
296                    BTreeMap::<String, BTreeMap<SelectorType, Vec<(String, String)>>>::new();
297
298                for (contract, artifact) in artifacts.by_ref() {
299                    let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
300
301                    let contract_selectors = selectors.entry(contract.clone()).or_default();
302
303                    for func in abi.functions() {
304                        let sig = func.signature();
305                        let selector = func.selector();
306                        contract_selectors
307                            .entry(SelectorType::Function)
308                            .or_default()
309                            .push((hex::encode_prefixed(selector), sig));
310                    }
311
312                    for event in abi.events() {
313                        let sig = event.signature();
314                        let selector = event.selector();
315                        contract_selectors
316                            .entry(SelectorType::Event)
317                            .or_default()
318                            .push((hex::encode_prefixed(selector), sig));
319                    }
320
321                    for error in abi.errors() {
322                        let sig = error.signature();
323                        let selector = error.selector();
324                        contract_selectors
325                            .entry(SelectorType::Error)
326                            .or_default()
327                            .push((hex::encode_prefixed(selector), sig));
328                    }
329                }
330
331                if no_group {
332                    let mut table = Table::new();
333                    if shell::is_markdown() {
334                        table.load_preset(ASCII_MARKDOWN);
335                    } else {
336                        table.apply_modifier(UTF8_ROUND_CORNERS);
337                    }
338                    table.set_header(["Type", "Signature", "Selector", "Contract"]);
339
340                    for (contract, contract_selectors) in selectors {
341                        for (selector_type, selectors) in contract_selectors {
342                            for (selector, sig) in selectors {
343                                table.add_row([
344                                    selector_type.to_string(),
345                                    sig,
346                                    selector,
347                                    contract.clone(),
348                                ]);
349                            }
350                        }
351                    }
352
353                    sh_println!("\n{table}")?;
354                } else {
355                    for (idx, (contract, contract_selectors)) in selectors.into_iter().enumerate() {
356                        sh_println!("{}{contract}", if idx == 0 { "" } else { "\n" })?;
357                        let mut table = Table::new();
358                        if shell::is_markdown() {
359                            table.load_preset(ASCII_MARKDOWN);
360                        } else {
361                            table.apply_modifier(UTF8_ROUND_CORNERS);
362                        }
363                        table.set_header(["Type", "Signature", "Selector"]);
364
365                        for (selector_type, selectors) in contract_selectors {
366                            for (selector, sig) in selectors {
367                                table.add_row([selector_type.to_string(), sig, selector]);
368                            }
369                        }
370                        sh_println!("\n{table}")?;
371                    }
372                }
373            }
374
375            Self::Find { selector, project_paths } => {
376                sh_status!("Searching for selector {selector:?} in the project...")?;
377
378                let (mut project, compiler) = project_from_paths(project_paths)?;
379                let outcome = compile_abi_project(&mut project, compiler.quiet(true))?;
380                let artifacts = outcome
381                    .into_artifacts_with_files()
382                    .filter(|(file, _, _)| {
383                        let is_sources_path = file.starts_with(&project.paths.sources);
384                        let is_test = file.is_sol_test();
385                        is_sources_path && !is_test
386                    })
387                    .collect::<Vec<_>>();
388
389                let mut table = Table::new();
390                if shell::is_markdown() {
391                    table.load_preset(ASCII_MARKDOWN);
392                } else {
393                    table.apply_modifier(UTF8_ROUND_CORNERS);
394                }
395
396                table.set_header(["Type", "Signature", "Selector", "Contract"]);
397
398                let selector_str = selector.strip_prefix("0x").unwrap_or(selector.as_str());
399                let selector_bytes = hex::decode(selector_str)?;
400
401                for (_file, contract, artifact) in artifacts {
402                    let abi = artifact.abi.ok_or_else(|| eyre::eyre!("Unable to fetch abi"))?;
403
404                    for func in abi.functions() {
405                        if func.selector().as_slice().starts_with(selector_bytes.as_slice()) {
406                            table.add_row([
407                                "Function",
408                                &func.signature(),
409                                &hex::encode_prefixed(func.selector()),
410                                contract.as_str(),
411                            ]);
412                        }
413                    }
414
415                    for event in abi.events() {
416                        if event.selector().as_slice().starts_with(selector_bytes.as_slice()) {
417                            table.add_row([
418                                "Event",
419                                &event.signature(),
420                                &hex::encode_prefixed(event.selector()),
421                                contract.as_str(),
422                            ]);
423                        }
424                    }
425
426                    for error in abi.errors() {
427                        if error.selector().as_slice().starts_with(selector_bytes.as_slice()) {
428                            table.add_row([
429                                "Error",
430                                &error.signature(),
431                                &hex::encode_prefixed(error.selector()),
432                                contract.as_str(),
433                            ]);
434                        }
435                    }
436                }
437
438                if table.row_count() > 0 {
439                    sh_status!("Found {} instance(s)...", table.row_count())?;
440                    sh_println!("\n{table}\n")?;
441                } else {
442                    return Err(eyre::eyre!("\nSelector not found in the project."));
443                }
444            }
445        }
446        Ok(())
447    }
448}
449
450fn project_from_paths(
451    project_paths: ProjectPathOpts,
452) -> Result<(Project<MultiCompiler>, ProjectCompiler)> {
453    let config = BuildOpts { project_paths, ..Default::default() }.load_config()?;
454    let compiler = ProjectCompiler::new().dynamic_test_linking(config.dynamic_test_linking);
455    let mut project = config.project()?;
456    if !project.build_info {
457        project.no_artifacts = true;
458    }
459    Ok((project, compiler))
460}