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