1use alloy_json_abi::JsonAbi;
2use alloy_primitives::Address;
3use eyre::{Result, WrapErr};
4use foundry_common::{
5 compile::ProjectCompiler, fs, selectors::SelectorKind, shell, ContractsByArtifact,
6 TestFunctionExt,
7};
8use foundry_compilers::{
9 artifacts::{CompactBytecode, Settings},
10 cache::{CacheEntry, CompilerCache},
11 utils::read_json_file,
12 Artifact, ArtifactId, ProjectCompileOutput,
13};
14use foundry_config::{error::ExtractConfigError, figment::Figment, Chain, Config, NamedChain};
15use foundry_debugger::Debugger;
16use foundry_evm::{
17 executors::{DeployResult, EvmError, RawCallResult},
18 opts::EvmOpts,
19 traces::{
20 debug::{ContractSources, DebugTraceIdentifier},
21 decode_trace_arena,
22 identifier::{SignaturesCache, SignaturesIdentifier, TraceIdentifiers},
23 render_trace_arena_inner, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
24 },
25};
26use std::{
27 fmt::Write,
28 path::{Path, PathBuf},
29 str::FromStr,
30};
31use yansi::Paint;
32
33#[track_caller]
36pub fn remove_contract(
37 output: ProjectCompileOutput,
38 path: &Path,
39 name: &str,
40) -> Result<(JsonAbi, CompactBytecode, ArtifactId)> {
41 let mut other = Vec::new();
42 let Some((id, contract)) = output.into_artifacts().find_map(|(id, artifact)| {
43 if id.name == name && id.source == path {
44 Some((id, artifact))
45 } else {
46 other.push(id.name);
47 None
48 }
49 }) else {
50 let mut err = format!("could not find artifact: `{name}`");
51 if let Some(suggestion) = super::did_you_mean(name, other).pop() {
52 if suggestion != name {
53 err = format!(
54 r#"{err}
55
56 Did you mean `{suggestion}`?"#
57 );
58 }
59 }
60 eyre::bail!(err)
61 };
62
63 let abi = contract
64 .get_abi()
65 .ok_or_else(|| eyre::eyre!("contract {} does not contain abi", name))?
66 .into_owned();
67
68 let bin = contract
69 .get_bytecode()
70 .ok_or_else(|| eyre::eyre!("contract {} does not contain bytecode", name))?
71 .into_owned();
72
73 Ok((abi, bin, id))
74}
75
76pub fn get_cached_entry_by_name(
80 cache: &CompilerCache<Settings>,
81 name: &str,
82) -> Result<(PathBuf, CacheEntry)> {
83 let mut cached_entry = None;
84 let mut alternatives = Vec::new();
85
86 for (abs_path, entry) in &cache.files {
87 for artifact_name in entry.artifacts.keys() {
88 if artifact_name == name {
89 if cached_entry.is_some() {
90 eyre::bail!(
91 "contract with duplicate name `{}`. please pass the path instead",
92 name
93 )
94 }
95 cached_entry = Some((abs_path.to_owned(), entry.to_owned()));
96 } else {
97 alternatives.push(artifact_name);
98 }
99 }
100 }
101
102 if let Some(entry) = cached_entry {
103 return Ok(entry);
104 }
105
106 let mut err = format!("could not find artifact: `{name}`");
107 if let Some(suggestion) = super::did_you_mean(name, &alternatives).pop() {
108 err = format!(
109 r#"{err}
110
111 Did you mean `{suggestion}`?"#
112 );
113 }
114 eyre::bail!(err)
115}
116
117pub fn ensure_clean_constructor(abi: &JsonAbi) -> Result<()> {
119 if let Some(constructor) = &abi.constructor {
120 if !constructor.inputs.is_empty() {
121 eyre::bail!("Contract constructor should have no arguments. Add those arguments to `run(...)` instead, and call it with `--sig run(...)`.");
122 }
123 }
124 Ok(())
125}
126
127pub fn needs_setup(abi: &JsonAbi) -> bool {
128 let setup_fns: Vec<_> = abi.functions().filter(|func| func.name.is_setup()).collect();
129
130 for setup_fn in &setup_fns {
131 if setup_fn.name != "setUp" {
132 let _ = sh_warn!(
133 "Found invalid setup function \"{}\" did you mean \"setUp()\"?",
134 setup_fn.signature()
135 );
136 }
137 }
138
139 setup_fns.len() == 1 && setup_fns[0].name == "setUp"
140}
141
142pub fn eta_key(state: &indicatif::ProgressState, f: &mut dyn Write) {
143 write!(f, "{:.1}s", state.eta().as_secs_f64()).unwrap()
144}
145
146pub fn init_progress(len: u64, label: &str) -> indicatif::ProgressBar {
147 let pb = indicatif::ProgressBar::new(len);
148 let mut template =
149 "{prefix}{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} "
150 .to_string();
151 write!(template, "{label}").unwrap();
152 template += " ({eta})";
153 pb.set_style(
154 indicatif::ProgressStyle::with_template(&template)
155 .unwrap()
156 .with_key("eta", crate::utils::eta_key)
157 .progress_chars("#>-"),
158 );
159 pb
160}
161
162pub fn has_different_gas_calc(chain_id: u64) -> bool {
164 if let Some(chain) = Chain::from(chain_id).named() {
165 return chain.is_arbitrum() ||
166 matches!(
167 chain,
168 NamedChain::Acala |
169 NamedChain::AcalaMandalaTestnet |
170 NamedChain::AcalaTestnet |
171 NamedChain::Etherlink |
172 NamedChain::EtherlinkTestnet |
173 NamedChain::Karura |
174 NamedChain::KaruraTestnet |
175 NamedChain::Mantle |
176 NamedChain::MantleSepolia |
177 NamedChain::MantleTestnet |
178 NamedChain::Moonbase |
179 NamedChain::Moonbeam |
180 NamedChain::MoonbeamDev |
181 NamedChain::Moonriver |
182 NamedChain::Metis |
183 NamedChain::Abstract |
184 NamedChain::ZkSync |
185 NamedChain::ZkSyncTestnet
186 );
187 }
188 false
189}
190
191pub fn has_batch_support(chain_id: u64) -> bool {
193 if let Some(chain) = Chain::from(chain_id).named() {
194 return !chain.is_arbitrum();
195 }
196 true
197}
198
199pub trait LoadConfig {
207 fn figment(&self) -> Figment;
209
210 fn load_config(&self) -> Result<Config, ExtractConfigError> {
212 self.load_config_no_warnings().inspect(emit_warnings)
213 }
214
215 fn load_config_no_warnings(&self) -> Result<Config, ExtractConfigError> {
217 self.load_config_unsanitized_no_warnings().map(Config::sanitized)
218 }
219
220 fn load_config_unsanitized(&self) -> Result<Config, ExtractConfigError> {
222 self.load_config_unsanitized_no_warnings().inspect(emit_warnings)
223 }
224
225 fn load_config_unsanitized_no_warnings(&self) -> Result<Config, ExtractConfigError> {
227 Config::from_provider(self.figment())
228 }
229
230 fn load_config_and_evm_opts(&self) -> Result<(Config, EvmOpts)> {
232 self.load_config_and_evm_opts_no_warnings().inspect(|(config, _)| emit_warnings(config))
233 }
234
235 fn load_config_and_evm_opts_no_warnings(&self) -> Result<(Config, EvmOpts)> {
237 let figment = self.figment();
238
239 let mut evm_opts = figment.extract::<EvmOpts>().map_err(ExtractConfigError::new)?;
240 let config = Config::from_provider(figment)?.sanitized();
241
242 if let Some(fork_url) = config.get_rpc_url() {
244 trace!(target: "forge::config", ?fork_url, "Update EvmOpts fork url");
245 evm_opts.fork_url = Some(fork_url?.into_owned());
246 }
247
248 Ok((config, evm_opts))
249 }
250}
251
252impl<T> LoadConfig for T
253where
254 for<'a> Figment: From<&'a T>,
255{
256 fn figment(&self) -> Figment {
257 self.into()
258 }
259}
260
261fn emit_warnings(config: &Config) {
262 for warning in &config.warnings {
263 let _ = sh_warn!("{warning}");
264 }
265}
266
267pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> Result<Vec<String>> {
269 if !constructor_args_path.exists() {
270 eyre::bail!("Constructor args file \"{}\" not found", constructor_args_path.display());
271 }
272 let args = if constructor_args_path.extension() == Some(std::ffi::OsStr::new("json")) {
273 read_json_file(&constructor_args_path).wrap_err(format!(
274 "Constructor args file \"{}\" must encode a json array",
275 constructor_args_path.display(),
276 ))?
277 } else {
278 fs::read_to_string(constructor_args_path)?.split_whitespace().map(str::to_string).collect()
279 };
280 Ok(args)
281}
282
283#[derive(Debug)]
285pub struct TraceResult {
286 pub success: bool,
287 pub traces: Option<Traces>,
288 pub gas_used: u64,
289}
290
291impl TraceResult {
292 pub fn from_raw(raw: RawCallResult, trace_kind: TraceKind) -> Self {
294 let RawCallResult { gas_used, traces, reverted, .. } = raw;
295 Self { success: !reverted, traces: traces.map(|arena| vec![(trace_kind, arena)]), gas_used }
296 }
297}
298
299impl From<DeployResult> for TraceResult {
300 fn from(result: DeployResult) -> Self {
301 Self::from_raw(result.raw, TraceKind::Deployment)
302 }
303}
304
305impl TryFrom<Result<DeployResult, EvmError>> for TraceResult {
306 type Error = EvmError;
307
308 fn try_from(value: Result<DeployResult, EvmError>) -> Result<Self, Self::Error> {
309 match value {
310 Ok(result) => Ok(Self::from(result)),
311 Err(EvmError::Execution(err)) => Ok(Self::from_raw(err.raw, TraceKind::Deployment)),
312 Err(err) => Err(err),
313 }
314 }
315}
316
317impl From<RawCallResult> for TraceResult {
318 fn from(result: RawCallResult) -> Self {
319 Self::from_raw(result, TraceKind::Execution)
320 }
321}
322
323impl TryFrom<Result<RawCallResult>> for TraceResult {
324 type Error = EvmError;
325
326 fn try_from(value: Result<RawCallResult>) -> Result<Self, Self::Error> {
327 match value {
328 Ok(result) => Ok(Self::from(result)),
329 Err(err) => Err(EvmError::from(err)),
330 }
331 }
332}
333
334pub async fn handle_traces(
336 mut result: TraceResult,
337 config: &Config,
338 chain: Option<Chain>,
339 labels: Vec<String>,
340 with_local_artifacts: bool,
341 debug: bool,
342 decode_internal: bool,
343) -> Result<()> {
344 let (known_contracts, mut sources) = if with_local_artifacts {
345 let _ = sh_println!("Compiling project to generate artifacts");
346 let project = config.project()?;
347 let compiler = ProjectCompiler::new();
348 let output = compiler.compile(&project)?;
349 (
350 Some(ContractsByArtifact::new(
351 output.artifact_ids().map(|(id, artifact)| (id, artifact.clone().into())),
352 )),
353 ContractSources::from_project_output(&output, project.root(), None)?,
354 )
355 } else {
356 (None, ContractSources::default())
357 };
358
359 let labels = labels.iter().filter_map(|label_str| {
360 let mut iter = label_str.split(':');
361
362 if let Some(addr) = iter.next() {
363 if let (Ok(address), Some(label)) = (Address::from_str(addr), iter.next()) {
364 return Some((address, label.to_string()));
365 }
366 }
367 None
368 });
369 let config_labels = config.labels.clone().into_iter();
370
371 let mut builder = CallTraceDecoderBuilder::new()
372 .with_labels(labels.chain(config_labels))
373 .with_signature_identifier(SignaturesIdentifier::from_config(config)?);
374 let mut identifier = TraceIdentifiers::new().with_etherscan(config, chain)?;
375 if let Some(contracts) = &known_contracts {
376 builder = builder.with_known_contracts(contracts);
377 identifier = identifier.with_local(contracts);
378 }
379
380 let mut decoder = builder.build();
381
382 for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
383 decoder.identify(trace, &mut identifier);
384 }
385
386 if decode_internal || debug {
387 if let Some(ref etherscan_identifier) = identifier.etherscan {
388 sources.merge(etherscan_identifier.get_compiled_contracts().await?);
389 }
390
391 if debug {
392 let mut debugger = Debugger::builder()
393 .traces(result.traces.expect("missing traces"))
394 .decoder(&decoder)
395 .sources(sources)
396 .build();
397 debugger.try_run_tui()?;
398 return Ok(())
399 }
400
401 decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
402 }
403
404 print_traces(&mut result, &decoder, shell::verbosity() > 0, shell::verbosity() > 4).await?;
405
406 Ok(())
407}
408
409pub async fn print_traces(
410 result: &mut TraceResult,
411 decoder: &CallTraceDecoder,
412 verbose: bool,
413 state_changes: bool,
414) -> Result<()> {
415 let traces = result.traces.as_mut().expect("No traces found");
416
417 if !shell::is_json() {
418 sh_println!("Traces:")?;
419 }
420
421 for (_, arena) in traces {
422 decode_trace_arena(arena, decoder).await;
423 sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
424 }
425
426 if shell::is_json() {
427 return Ok(());
428 }
429
430 sh_println!()?;
431 if result.success {
432 sh_println!("{}", "Transaction successfully executed.".green())?;
433 } else {
434 sh_err!("Transaction failed.")?;
435 }
436 sh_println!("Gas used: {}", result.gas_used)?;
437
438 Ok(())
439}
440
441pub fn cache_local_signatures(output: &ProjectCompileOutput, cache_dir: &Path) -> Result<()> {
444 let path = cache_dir.join("signatures");
445 let mut signatures = SignaturesCache::load(&path);
446 for (_, artifact) in output.artifacts() {
447 if let Some(abi) = &artifact.abi {
448 signatures.extend_from_abi(abi);
449 }
450
451 if let Some(method_identifiers) = &artifact.method_identifiers {
453 signatures.extend(method_identifiers.iter().filter_map(|(signature, selector)| {
454 Some((SelectorKind::Function(selector.parse().ok()?), signature.clone()))
455 }));
456 }
457 }
458 signatures.save(&path);
459 Ok(())
460}