Skip to main content

forge/cmd/
selectors.rs

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