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