1use crate::revm::primitives::Eof;
2use alloy_json_abi::{EventParam, InternalType, JsonAbi, Param};
3use alloy_primitives::{hex, keccak256, Address};
4use clap::Parser;
5use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Cell, Table};
6use eyre::{Context, Result};
7use foundry_cli::opts::{BuildOpts, CompilerOpts};
8use foundry_common::{
9 compile::{PathOrContractInfo, ProjectCompiler},
10 find_matching_contract_artifact, find_target_path,
11 fmt::pretty_eof,
12 shell,
13};
14use foundry_compilers::artifacts::{
15 output_selection::{
16 BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
17 EvmOutputSelection, EwasmOutputSelection,
18 },
19 CompactBytecode, StorageLayout,
20};
21use regex::Regex;
22use serde_json::{Map, Value};
23use std::{collections::BTreeMap, fmt, str::FromStr, sync::LazyLock};
24
25#[derive(Clone, Debug, Parser)]
27pub struct InspectArgs {
28 #[arg(value_parser = PathOrContractInfo::from_str)]
30 pub contract: PathOrContractInfo,
31
32 #[arg(value_enum)]
34 pub field: ContractArtifactField,
35
36 #[command(flatten)]
38 build: BuildOpts,
39
40 #[arg(long, short, help_heading = "Display options")]
42 pub strip_yul_comments: bool,
43}
44
45impl InspectArgs {
46 pub fn run(self) -> Result<()> {
47 let Self { contract, field, build, strip_yul_comments } = self;
48
49 trace!(target: "forge", ?field, ?contract, "running forge inspect");
50
51 let mut cos = build.compiler.extra_output;
53 if !field.is_default() && !cos.iter().any(|selected| field == *selected) {
54 cos.push(field.into());
55 }
56
57 let optimized = if field == ContractArtifactField::AssemblyOptimized {
59 Some(true)
60 } else {
61 build.compiler.optimize
62 };
63
64 let modified_build_args = BuildOpts {
66 compiler: CompilerOpts { extra_output: cos, optimize: optimized, ..build.compiler },
67 ..build
68 };
69
70 let project = modified_build_args.project()?;
72 let compiler = ProjectCompiler::new().quiet(true);
73 let target_path = find_target_path(&project, &contract)?;
74 let mut output = compiler.files([target_path.clone()]).compile(&project)?;
75
76 let artifact = find_matching_contract_artifact(&mut output, &target_path, contract.name())?;
78
79 match field {
81 ContractArtifactField::Abi => {
82 let abi = artifact
83 .abi
84 .as_ref()
85 .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
86 print_abi(abi)?;
87 }
88 ContractArtifactField::Bytecode => {
89 print_json_str(&artifact.bytecode, Some("object"))?;
90 }
91 ContractArtifactField::DeployedBytecode => {
92 print_json_str(&artifact.deployed_bytecode, Some("object"))?;
93 }
94 ContractArtifactField::Assembly | ContractArtifactField::AssemblyOptimized => {
95 print_json_str(&artifact.assembly, None)?;
96 }
97 ContractArtifactField::LegacyAssembly => {
98 print_json_str(&artifact.legacy_assembly, None)?;
99 }
100 ContractArtifactField::MethodIdentifiers => {
101 print_method_identifiers(&artifact.method_identifiers)?;
102 }
103 ContractArtifactField::GasEstimates => {
104 print_json(&artifact.gas_estimates)?;
105 }
106 ContractArtifactField::StorageLayout => {
107 print_storage_layout(artifact.storage_layout.as_ref())?;
108 }
109 ContractArtifactField::DevDoc => {
110 print_json(&artifact.devdoc)?;
111 }
112 ContractArtifactField::Ir => {
113 print_yul(artifact.ir.as_deref(), strip_yul_comments)?;
114 }
115 ContractArtifactField::IrOptimized => {
116 print_yul(artifact.ir_optimized.as_deref(), strip_yul_comments)?;
117 }
118 ContractArtifactField::Metadata => {
119 print_json(&artifact.metadata)?;
120 }
121 ContractArtifactField::UserDoc => {
122 print_json(&artifact.userdoc)?;
123 }
124 ContractArtifactField::Ewasm => {
125 print_json_str(&artifact.ewasm, None)?;
126 }
127 ContractArtifactField::Errors => {
128 let out = artifact.abi.as_ref().map_or(Map::new(), parse_errors);
129 print_errors_events(&out, true)?;
130 }
131 ContractArtifactField::Events => {
132 let out = artifact.abi.as_ref().map_or(Map::new(), parse_events);
133 print_errors_events(&out, false)?;
134 }
135 ContractArtifactField::Eof => {
136 print_eof(artifact.deployed_bytecode.and_then(|b| b.bytecode))?;
137 }
138 ContractArtifactField::EofInit => {
139 print_eof(artifact.bytecode)?;
140 }
141 };
142
143 Ok(())
144 }
145}
146
147fn parse_errors(abi: &JsonAbi) -> Map<String, Value> {
148 let mut out = serde_json::Map::new();
149 for er in abi.errors.iter().flat_map(|(_, errors)| errors) {
150 let types = get_ty_sig(&er.inputs);
151 let sig = format!("{:x}", er.selector());
152 let sig_trimmed = &sig[0..8];
153 out.insert(format!("{}({})", er.name, types), sig_trimmed.to_string().into());
154 }
155 out
156}
157
158fn parse_events(abi: &JsonAbi) -> Map<String, Value> {
159 let mut out = serde_json::Map::new();
160 for ev in abi.events.iter().flat_map(|(_, events)| events) {
161 let types = parse_event_params(&ev.inputs);
162 let topic = hex::encode(keccak256(ev.signature()));
163 out.insert(format!("{}({})", ev.name, types), format!("0x{topic}").into());
164 }
165 out
166}
167
168fn parse_event_params(ev_params: &[EventParam]) -> String {
169 ev_params
170 .iter()
171 .map(|p| {
172 if let Some(ty) = p.internal_type() {
173 return internal_ty(ty)
174 }
175 p.ty.clone()
176 })
177 .collect::<Vec<_>>()
178 .join(",")
179}
180
181fn print_abi(abi: &JsonAbi) -> Result<()> {
182 if shell::is_json() {
183 return print_json(abi)
184 }
185
186 let headers = vec![Cell::new("Type"), Cell::new("Signature"), Cell::new("Selector")];
187 print_table(headers, |table| {
188 for ev in abi.events.iter().flat_map(|(_, events)| events) {
190 let types = parse_event_params(&ev.inputs);
191 let selector = ev.selector().to_string();
192 table.add_row(["event", &format!("{}({})", ev.name, types), &selector]);
193 }
194
195 for er in abi.errors.iter().flat_map(|(_, errors)| errors) {
197 let selector = er.selector().to_string();
198 table.add_row([
199 "error",
200 &format!("{}({})", er.name, get_ty_sig(&er.inputs)),
201 &selector,
202 ]);
203 }
204
205 for func in abi.functions.iter().flat_map(|(_, f)| f) {
207 let selector = func.selector().to_string();
208 let state_mut = func.state_mutability.as_json_str();
209 let func_sig = if !func.outputs.is_empty() {
210 format!(
211 "{}({}) {state_mut} returns ({})",
212 func.name,
213 get_ty_sig(&func.inputs),
214 get_ty_sig(&func.outputs)
215 )
216 } else {
217 format!("{}({}) {state_mut}", func.name, get_ty_sig(&func.inputs))
218 };
219 table.add_row(["function", &func_sig, &selector]);
220 }
221
222 if let Some(constructor) = abi.constructor() {
223 let state_mut = constructor.state_mutability.as_json_str();
224 table.add_row([
225 "constructor",
226 &format!("constructor({}) {state_mut}", get_ty_sig(&constructor.inputs)),
227 "",
228 ]);
229 }
230
231 if let Some(fallback) = &abi.fallback {
232 let state_mut = fallback.state_mutability.as_json_str();
233 table.add_row(["fallback", &format!("fallback() {state_mut}"), ""]);
234 }
235
236 if let Some(receive) = &abi.receive {
237 let state_mut = receive.state_mutability.as_json_str();
238 table.add_row(["receive", &format!("receive() {state_mut}"), ""]);
239 }
240 })
241}
242
243fn get_ty_sig(inputs: &[Param]) -> String {
244 inputs
245 .iter()
246 .map(|p| {
247 if let Some(ty) = p.internal_type() {
248 return internal_ty(ty);
249 }
250 p.ty.clone()
251 })
252 .collect::<Vec<_>>()
253 .join(",")
254}
255
256fn internal_ty(ty: &InternalType) -> String {
257 let contract_ty =
258 |c: Option<&str>, ty: &String| c.map_or_else(|| ty.clone(), |c| format!("{c}.{ty}"));
259 match ty {
260 InternalType::AddressPayable(addr) => addr.clone(),
261 InternalType::Contract(contract) => contract.clone(),
262 InternalType::Enum { contract, ty } => contract_ty(contract.as_deref(), ty),
263 InternalType::Struct { contract, ty } => contract_ty(contract.as_deref(), ty),
264 InternalType::Other { contract, ty } => contract_ty(contract.as_deref(), ty),
265 }
266}
267
268pub fn print_storage_layout(storage_layout: Option<&StorageLayout>) -> Result<()> {
269 let Some(storage_layout) = storage_layout else {
270 eyre::bail!("Could not get storage layout");
271 };
272
273 if shell::is_json() {
274 return print_json(&storage_layout)
275 }
276
277 let headers = vec![
278 Cell::new("Name"),
279 Cell::new("Type"),
280 Cell::new("Slot"),
281 Cell::new("Offset"),
282 Cell::new("Bytes"),
283 Cell::new("Contract"),
284 ];
285
286 print_table(headers, |table| {
287 for slot in &storage_layout.storage {
288 let storage_type = storage_layout.types.get(&slot.storage_type);
289 table.add_row([
290 slot.label.as_str(),
291 storage_type.map_or("?", |t| &t.label),
292 &slot.slot,
293 &slot.offset.to_string(),
294 storage_type.map_or("?", |t| &t.number_of_bytes),
295 &slot.contract,
296 ]);
297 }
298 })
299}
300
301fn print_method_identifiers(method_identifiers: &Option<BTreeMap<String, String>>) -> Result<()> {
302 let Some(method_identifiers) = method_identifiers else {
303 eyre::bail!("Could not get method identifiers");
304 };
305
306 if shell::is_json() {
307 return print_json(method_identifiers)
308 }
309
310 let headers = vec![Cell::new("Method"), Cell::new("Identifier")];
311
312 print_table(headers, |table| {
313 for (method, identifier) in method_identifiers {
314 table.add_row([method, identifier]);
315 }
316 })
317}
318
319fn print_errors_events(map: &Map<String, Value>, is_err: bool) -> Result<()> {
320 if shell::is_json() {
321 return print_json(map);
322 }
323
324 let headers = if is_err {
325 vec![Cell::new("Error"), Cell::new("Selector")]
326 } else {
327 vec![Cell::new("Event"), Cell::new("Topic")]
328 };
329 print_table(headers, |table| {
330 for (method, selector) in map {
331 table.add_row([method, selector.as_str().unwrap()]);
332 }
333 })
334}
335
336fn print_table(headers: Vec<Cell>, add_rows: impl FnOnce(&mut Table)) -> Result<()> {
337 let mut table = Table::new();
338 table.apply_modifier(UTF8_ROUND_CORNERS);
339 table.set_header(headers);
340 add_rows(&mut table);
341 sh_println!("\n{table}\n")?;
342 Ok(())
343}
344
345#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
347pub enum ContractArtifactField {
348 Abi,
349 Bytecode,
350 DeployedBytecode,
351 Assembly,
352 AssemblyOptimized,
353 LegacyAssembly,
354 MethodIdentifiers,
355 GasEstimates,
356 StorageLayout,
357 DevDoc,
358 Ir,
359 IrOptimized,
360 Metadata,
361 UserDoc,
362 Ewasm,
363 Errors,
364 Events,
365 Eof,
366 EofInit,
367}
368
369macro_rules! impl_value_enum {
370 (enum $name:ident { $($field:ident => $main:literal $(| $alias:literal)*),+ $(,)? }) => {
371 impl $name {
372 pub const ALL: &'static [Self] = &[$(Self::$field),+];
374
375 #[inline]
377 pub const fn as_str(&self) -> &'static str {
378 match self {
379 $(
380 Self::$field => $main,
381 )+
382 }
383 }
384
385 #[inline]
387 pub const fn aliases(&self) -> &'static [&'static str] {
388 match self {
389 $(
390 Self::$field => &[$($alias),*],
391 )+
392 }
393 }
394 }
395
396 impl ::clap::ValueEnum for $name {
397 #[inline]
398 fn value_variants<'a>() -> &'a [Self] {
399 Self::ALL
400 }
401
402 #[inline]
403 fn to_possible_value(&self) -> Option<::clap::builder::PossibleValue> {
404 Some(::clap::builder::PossibleValue::new(Self::as_str(self)).aliases(Self::aliases(self)))
405 }
406
407 #[inline]
408 fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
409 let _ = ignore_case;
410 <Self as ::std::str::FromStr>::from_str(input)
411 }
412 }
413
414 impl ::std::str::FromStr for $name {
415 type Err = String;
416
417 fn from_str(s: &str) -> Result<Self, Self::Err> {
418 match s {
419 $(
420 $main $(| $alias)* => Ok(Self::$field),
421 )+
422 _ => Err(format!(concat!("Invalid ", stringify!($name), " value: {}"), s)),
423 }
424 }
425 }
426 };
427}
428
429impl_value_enum! {
430 enum ContractArtifactField {
431 Abi => "abi",
432 Bytecode => "bytecode" | "bytes" | "b",
433 DeployedBytecode => "deployedBytecode" | "deployed_bytecode" | "deployed-bytecode"
434 | "deployed" | "deployedbytecode",
435 Assembly => "assembly" | "asm",
436 LegacyAssembly => "legacyAssembly" | "legacyassembly" | "legacy_assembly",
437 AssemblyOptimized => "assemblyOptimized" | "asmOptimized" | "assemblyoptimized"
438 | "assembly_optimized" | "asmopt" | "assembly-optimized"
439 | "asmo" | "asm-optimized" | "asmoptimized" | "asm_optimized",
440 MethodIdentifiers => "methodIdentifiers" | "methodidentifiers" | "methods"
441 | "method_identifiers" | "method-identifiers" | "mi",
442 GasEstimates => "gasEstimates" | "gas" | "gas_estimates" | "gas-estimates"
443 | "gasestimates",
444 StorageLayout => "storageLayout" | "storage_layout" | "storage-layout"
445 | "storagelayout" | "storage",
446 DevDoc => "devdoc" | "dev-doc" | "devDoc",
447 Ir => "ir" | "iR" | "IR",
448 IrOptimized => "irOptimized" | "ir-optimized" | "iroptimized" | "iro" | "iropt",
449 Metadata => "metadata" | "meta",
450 UserDoc => "userdoc" | "userDoc" | "user-doc",
451 Ewasm => "ewasm" | "e-wasm",
452 Errors => "errors" | "er",
453 Events => "events" | "ev",
454 Eof => "eof" | "eof-container" | "eof-deployed",
455 EofInit => "eof-init" | "eof-initcode" | "eof-initcontainer",
456 }
457}
458
459impl From<ContractArtifactField> for ContractOutputSelection {
460 fn from(field: ContractArtifactField) -> Self {
461 type Caf = ContractArtifactField;
462 match field {
463 Caf::Abi => Self::Abi,
464 Caf::Bytecode => Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)),
465 Caf::DeployedBytecode => Self::Evm(EvmOutputSelection::DeployedByteCode(
466 DeployedBytecodeOutputSelection::All,
467 )),
468 Caf::Assembly | Caf::AssemblyOptimized => Self::Evm(EvmOutputSelection::Assembly),
469 Caf::LegacyAssembly => Self::Evm(EvmOutputSelection::LegacyAssembly),
470 Caf::MethodIdentifiers => Self::Evm(EvmOutputSelection::MethodIdentifiers),
471 Caf::GasEstimates => Self::Evm(EvmOutputSelection::GasEstimates),
472 Caf::StorageLayout => Self::StorageLayout,
473 Caf::DevDoc => Self::DevDoc,
474 Caf::Ir => Self::Ir,
475 Caf::IrOptimized => Self::IrOptimized,
476 Caf::Metadata => Self::Metadata,
477 Caf::UserDoc => Self::UserDoc,
478 Caf::Ewasm => Self::Ewasm(EwasmOutputSelection::All),
479 Caf::Errors => Self::Abi,
480 Caf::Events => Self::Abi,
481 Caf::Eof => Self::Evm(EvmOutputSelection::DeployedByteCode(
482 DeployedBytecodeOutputSelection::All,
483 )),
484 Caf::EofInit => Self::Evm(EvmOutputSelection::ByteCode(BytecodeOutputSelection::All)),
485 }
486 }
487}
488
489impl PartialEq<ContractOutputSelection> for ContractArtifactField {
490 fn eq(&self, other: &ContractOutputSelection) -> bool {
491 type Cos = ContractOutputSelection;
492 type Eos = EvmOutputSelection;
493 matches!(
494 (self, other),
495 (Self::Abi | Self::Events, Cos::Abi) |
496 (Self::Errors, Cos::Abi) |
497 (Self::Bytecode, Cos::Evm(Eos::ByteCode(_))) |
498 (Self::DeployedBytecode, Cos::Evm(Eos::DeployedByteCode(_))) |
499 (Self::Assembly | Self::AssemblyOptimized, Cos::Evm(Eos::Assembly)) |
500 (Self::LegacyAssembly, Cos::Evm(Eos::LegacyAssembly)) |
501 (Self::MethodIdentifiers, Cos::Evm(Eos::MethodIdentifiers)) |
502 (Self::GasEstimates, Cos::Evm(Eos::GasEstimates)) |
503 (Self::StorageLayout, Cos::StorageLayout) |
504 (Self::DevDoc, Cos::DevDoc) |
505 (Self::Ir, Cos::Ir) |
506 (Self::IrOptimized, Cos::IrOptimized) |
507 (Self::Metadata, Cos::Metadata) |
508 (Self::UserDoc, Cos::UserDoc) |
509 (Self::Ewasm, Cos::Ewasm(_)) |
510 (Self::Eof, Cos::Evm(Eos::DeployedByteCode(_))) |
511 (Self::EofInit, Cos::Evm(Eos::ByteCode(_)))
512 )
513 }
514}
515
516impl fmt::Display for ContractArtifactField {
517 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 f.write_str(self.as_str())
519 }
520}
521
522impl ContractArtifactField {
523 pub const fn is_default(&self) -> bool {
525 matches!(self, Self::Bytecode | Self::DeployedBytecode)
526 }
527}
528
529fn print_json(obj: &impl serde::Serialize) -> Result<()> {
530 sh_println!("{}", serde_json::to_string_pretty(obj)?)?;
531 Ok(())
532}
533
534fn print_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<()> {
535 sh_println!("{}", get_json_str(obj, key)?)?;
536 Ok(())
537}
538
539fn print_yul(yul: Option<&str>, strip_comments: bool) -> Result<()> {
540 let Some(yul) = yul else {
541 eyre::bail!("Could not get IR output");
542 };
543
544 static YUL_COMMENTS: LazyLock<Regex> =
545 LazyLock::new(|| Regex::new(r"(///.*\n\s*)|(\s*/\*\*.*?\*/)").unwrap());
546
547 if strip_comments {
548 sh_println!("{}", YUL_COMMENTS.replace_all(yul, ""))?;
549 } else {
550 sh_println!("{yul}")?;
551 }
552
553 Ok(())
554}
555
556fn get_json_str(obj: &impl serde::Serialize, key: Option<&str>) -> Result<String> {
557 let value = serde_json::to_value(obj)?;
558 let mut value_ref = &value;
559 if let Some(key) = key {
560 if let Some(value2) = value.get(key) {
561 value_ref = value2;
562 }
563 }
564 let s = match value_ref.as_str() {
565 Some(s) => s.to_string(),
566 None => format!("{value_ref:#}"),
567 };
568 Ok(s)
569}
570
571fn print_eof(bytecode: Option<CompactBytecode>) -> Result<()> {
573 let Some(mut bytecode) = bytecode else { eyre::bail!("No bytecode") };
574
575 if bytecode.object.is_unlinked() {
577 for (file, references) in bytecode.link_references.clone() {
578 for (name, _) in references {
579 bytecode.link(&file, &name, Address::ZERO);
580 }
581 }
582 }
583
584 let Some(bytecode) = bytecode.object.into_bytes() else {
585 eyre::bail!("Failed to link bytecode");
586 };
587
588 let eof = Eof::decode(bytecode).wrap_err("Failed to decode EOF")?;
589
590 sh_println!("{}", pretty_eof(&eof)?)?;
591
592 Ok(())
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn contract_output_selection() {
601 for &field in ContractArtifactField::ALL {
602 let selection: ContractOutputSelection = field.into();
603 assert_eq!(field, selection);
604
605 let s = field.as_str();
606 assert_eq!(s, field.to_string());
607 assert_eq!(s.parse::<ContractArtifactField>().unwrap(), field);
608 for alias in field.aliases() {
609 assert_eq!(alias.parse::<ContractArtifactField>().unwrap(), field);
610 }
611 }
612 }
613}