1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
3use alloy_network::{AnyNetwork, AnyRpcBlock};
4use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
5use alloy_provider::{Provider, network::BlockResponse};
6use alloy_rpc_types::BlockId;
7use clap::ValueEnum;
8use eyre::{OptionExt, Result};
9use foundry_block_explorers::{
10 contract::{ContractCreationData, ContractMetadata, Metadata},
11 errors::EtherscanError,
12 utils::lookup_compiler_version,
13};
14use foundry_cli::utils::LoadConfig;
15use foundry_common::{
16 abi::encode_args, compile::ProjectCompiler, find_matching_contract_artifact,
17 ignore_metadata_hash, shell,
18};
19use foundry_compilers::{
20 Graph,
21 artifacts::{BytecodeHash, CompactContractBytecode},
22 compilers::ParsedSource,
23 multi::{MultiCompilerLanguage, MultiCompilerParser},
24 utils::canonicalize,
25};
26use foundry_config::Config;
27use foundry_evm::{
28 constants::DEFAULT_CREATE2_DEPLOYER,
29 core::{
30 FoundryBlock as _,
31 decode::RevertDecoder,
32 evm::{BlockEnvFor, ChainFor, EvmEnvFor, FoundryEvmNetwork, TxEnvFor},
33 },
34 executors::{ExecutorBuilder, TracingExecutor},
35 opts::EvmOpts,
36 traces::TraceRequirements,
37 utils::{apply_chain_and_block_specific_env_changes_for_chain, block_env_from_header},
38};
39use foundry_evm_networks::NetworkConfigs;
40use reqwest::Url;
41use revm::{bytecode::Bytecode, context::Block as _, database::Database};
42use semver::{BuildMetadata, Version};
43use serde::{Deserialize, Serialize};
44use yansi::Paint;
45
46#[cfg(all(test, feature = "monad"))]
47use foundry_config::FoundryHardfork;
48
49#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
51pub enum BytecodeType {
52 #[serde(rename = "creation")]
53 Creation,
54 #[serde(rename = "runtime")]
55 Runtime,
56}
57
58impl BytecodeType {
59 pub const fn is_creation(&self) -> bool {
61 matches!(self, Self::Creation)
62 }
63
64 pub const fn is_runtime(&self) -> bool {
66 matches!(self, Self::Runtime)
67 }
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct JsonResult {
72 pub bytecode_type: BytecodeType,
73 pub match_type: Option<VerificationType>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub message: Option<String>,
76}
77
78pub fn match_bytecodes(
79 local_bytecode: &[u8],
80 bytecode: &[u8],
81 constructor_args: &[u8],
82 is_runtime: bool,
83 bytecode_hash: BytecodeHash,
84) -> Option<VerificationType> {
85 if local_bytecode == bytecode {
87 if bytecode_hash == BytecodeHash::None {
90 return Some(VerificationType::Partial);
91 }
92
93 Some(VerificationType::Full)
94 } else {
95 is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
96 .then_some(VerificationType::Partial)
97 }
98}
99
100pub fn build_project(
101 args: &VerifyBytecodeArgs,
102 config: &Config,
103) -> Result<CompactContractBytecode> {
104 let project = config.project()?;
105 let compiler = ProjectCompiler::new().quiet(true);
106
107 let target_path = match args.contract.path() {
108 Some(path) => Some(canonicalize(project.root().join(path))?),
109 None => Graph::<MultiCompilerParser>::resolve(&project.paths).ok().and_then(|graph| {
110 if graph
111 .nodes
112 .iter()
113 .any(|node| matches!(node.data.language(), MultiCompilerLanguage::Vyper(_)))
114 {
115 return None;
116 }
117 let mut matches = graph.nodes.iter().filter(|node| {
118 node.data.contract_names().iter().any(|name| name == &args.contract.name)
119 });
120 let target = matches.next()?;
121 (matches.next().is_none()
122 && graph.input_nodes().any(|input| input.path() == target.path()))
123 .then(|| target.path().to_path_buf())
124 }),
125 };
126 if let Some(target_path) = target_path {
127 let mut output = compiler.files([target_path.clone()]).compile(&project)?;
128 let artifact =
129 find_matching_contract_artifact(&mut output, &target_path, Some(&args.contract.name))?;
130 return Ok(artifact.into_contract_bytecode());
131 }
132
133 let mut output = compiler.compile(&project)?;
134
135 let artifact = output
136 .remove_contract(&args.contract)
137 .ok_or_eyre("Build Error: Contract artifact not found locally")?;
138
139 Ok(artifact.into_contract_bytecode())
140}
141
142pub fn print_result(
143 res: Option<VerificationType>,
144 bytecode_type: BytecodeType,
145 json_results: &mut Vec<JsonResult>,
146 etherscan_metadata: Option<&Metadata>,
147 config: &Config,
148) {
149 if let Some(res) = res {
150 if shell::is_json() {
151 let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
152 json_results.push(json_res);
153 } else {
154 let _ = sh_println!(
155 "{} with status {}",
156 format!("{bytecode_type:?} code matched").green().bold(),
157 res.green().bold()
158 );
159 }
160 } else if !shell::is_json() {
161 let _ = sh_err!(
162 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
163 );
164 if let Some(etherscan_metadata) = etherscan_metadata {
165 let mismatches = find_mismatch_in_settings(etherscan_metadata, config);
166 for mismatch in mismatches {
167 let _ = sh_eprintln!("{}", mismatch.red().bold());
168 }
169 }
170 } else {
171 let json_res = JsonResult {
172 bytecode_type,
173 match_type: res,
174 message: Some(format!(
175 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
176 )),
177 };
178 json_results.push(json_res);
179 }
180}
181
182fn is_partial_match(
183 mut local_bytecode: &[u8],
184 mut bytecode: &[u8],
185 constructor_args: &[u8],
186 is_runtime: bool,
187) -> bool {
188 if constructor_args.is_empty() || is_runtime {
190 return try_extract_and_compare_bytecode(local_bytecode, bytecode);
192 }
193
194 if !bytecode.ends_with(constructor_args) {
198 return false;
199 }
200
201 bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
203 local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
204
205 try_extract_and_compare_bytecode(local_bytecode, bytecode)
206}
207
208fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
209 local_bytecode = ignore_metadata_hash(local_bytecode);
210 bytecode = ignore_metadata_hash(bytecode);
211
212 local_bytecode == bytecode
214}
215
216fn find_mismatch_in_settings(
217 etherscan_settings: &Metadata,
218 local_settings: &Config,
219) -> Vec<String> {
220 let mut mismatches: Vec<String> = vec![];
221 if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
222 let str = format!(
223 "EVM version mismatch: local={}, onchain={}",
224 local_settings.evm_version, etherscan_settings.evm_version
225 );
226 mismatches.push(str);
227 }
228 let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
229 if etherscan_settings.optimization_used != local_optimizer {
230 let str = format!(
231 "Optimizer mismatch: local={}, onchain={}",
232 local_settings.optimizer.unwrap_or(false),
233 etherscan_settings.optimization_used
234 );
235 mismatches.push(str);
236 }
237 if let Some(local_runs) = local_settings.optimizer_runs
240 && etherscan_settings.runs != local_runs as u64
241 {
242 let str = format!(
243 "Optimizer runs mismatch: local={local_runs}, onchain={}",
244 etherscan_settings.runs
245 );
246 mismatches.push(str);
247 }
248
249 mismatches
250}
251
252pub fn maybe_predeploy_contract(
253 creation_data: Result<ContractCreationData, EtherscanError>,
254) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
255 let mut maybe_predeploy = false;
256 match creation_data {
257 Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
258 Err(EtherscanError::EmptyResult { status, message })
260 if status == "1" && message == "OK" =>
261 {
262 maybe_predeploy = true;
263 Ok((None, maybe_predeploy))
264 }
265 Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
267 maybe_predeploy = true;
268 Ok((None, maybe_predeploy))
269 }
270 Err(e) => {
271 eyre::bail!("Error fetching creation data from verifier-url: {:?}", e);
272 }
273 }
274}
275
276pub fn check_and_encode_args(
277 artifact: &CompactContractBytecode,
278 args: Vec<String>,
279) -> Result<Vec<u8>, eyre::ErrReport> {
280 let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) else {
281 if args.is_empty() {
282 return Ok(Vec::new());
283 }
284 eyre::bail!("Contract has no constructor arguments, but arguments were provided");
285 };
286 if constructor.inputs.len() != args.len() {
287 eyre::bail!(
288 "Mismatch of constructor arguments length. Expected {}, got {}",
289 constructor.inputs.len(),
290 args.len()
291 );
292 }
293 encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode_params())
294}
295
296pub fn validate_encoded_constructor_args(
297 artifact: &CompactContractBytecode,
298 args: Vec<u8>,
299) -> Result<Vec<u8>, eyre::ErrReport> {
300 let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) else {
301 if args.is_empty() {
302 return Ok(args);
303 }
304 eyre::bail!("Contract has no constructor arguments, but encoded arguments were provided");
305 };
306 let values = constructor
307 .abi_decode_input(&args)
308 .map_err(|err| eyre::eyre!("Invalid ABI-encoded constructor arguments: {err}"))?;
309 let encoded = constructor
310 .abi_encode_input(&values)
311 .map_err(|err| eyre::eyre!("Invalid ABI-encoded constructor arguments: {err}"))?;
312 if encoded != args {
313 eyre::bail!("Constructor arguments are not canonically ABI-encoded");
314 }
315 Ok(args)
316}
317
318pub fn check_explorer_args(source_code: &ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
319 if let Some(args) = source_code.items.first() {
320 Ok(args.constructor_arguments.clone())
321 } else {
322 eyre::bail!("No constructor arguments found from block explorer");
323 }
324}
325
326pub fn check_args_len(
327 artifact: &CompactContractBytecode,
328 args: &Bytes,
329) -> Result<(), eyre::ErrReport> {
330 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
331 && !constructor.inputs.is_empty()
332 && args.is_empty()
333 {
334 eyre::bail!(
335 "Contract expects {} constructor argument(s), but none were provided",
336 constructor.inputs.len()
337 );
338 }
339 Ok(())
340}
341
342pub fn load_fork_config_and_evm_opts(config: &Config) -> Result<(Config, EvmOpts)> {
343 let chain = config.chain;
344 let mut fork_config = config.clone();
345 fork_config.chain = None;
346
347 let (mut fork_config, mut evm_opts) = fork_config.load_config_and_evm_opts()?;
348 fork_config.chain = chain;
349 if let Some(chain) = chain {
350 evm_opts.env.chain_id = Some(chain.id());
351 }
352
353 Ok((fork_config, evm_opts))
354}
355
356pub async fn get_tracing_executor<FEN>(
357 fork_config: &mut Config,
358 fork_blk_num: u64,
359 execution_blk_num: u64,
360 execution_block: Option<&AnyRpcBlock>,
361 evm_opts: EvmOpts,
362 executor_builder: ExecutorBuilder<FEN>,
363) -> Result<(EvmEnvFor<FEN>, TxEnvFor<FEN>, TracingExecutor<FEN>)>
364where
365 FEN: FoundryEvmNetwork,
366{
367 fork_config.fork_block_number = Some(fork_blk_num);
368
369 let create2_deployer = evm_opts.create2_deployer;
370 let mut fork = TracingExecutor::<FEN>::get_fork(fork_config, evm_opts).await?;
371 let context = fork.context();
372
373 fork.evm_env.block_env.set_number(U256::from(execution_blk_num));
374 if let Some(block) = execution_block {
375 configure_env_block::<FEN>(
376 &mut fork.evm_env,
377 block,
378 context.chain().id(),
379 context.networks(),
380 );
381 }
382 fork.resolve_spec(fork_config, None);
383 fork.extend_precompile_labels(fork_config);
384
385 let evm_env = fork.evm_env.clone();
386 let tx_env = fork.tx_env.clone();
387 let executor = fork.into_executor(
388 executor_builder,
389 TraceRequirements::none().with_calls(true),
390 create2_deployer,
391 None,
392 )?;
393
394 Ok((evm_env, tx_env, executor))
395}
396
397#[cfg(all(test, feature = "monad"))]
398fn resolve_runtime_spec<FEN>(
399 config: &Config,
400 source_chain_id: ChainId,
401 endpoint_hardfork: Option<FoundryHardfork>,
402 evm_env: &mut EvmEnvFor<FEN>,
403) -> Option<FoundryHardfork>
404where
405 FEN: FoundryEvmNetwork,
406{
407 TracingExecutor::<FEN>::resolve_spec_for_chain(
408 config,
409 source_chain_id,
410 endpoint_hardfork,
411 evm_env,
412 None,
413 )
414}
415
416pub fn configure_env_block<FEN>(
417 evm_env: &mut EvmEnvFor<FEN>,
418 block: &AnyRpcBlock,
419 source_chain_id: ChainId,
420 config: NetworkConfigs,
421) where
422 FEN: FoundryEvmNetwork,
423{
424 let number = evm_env.block_env.number();
425 evm_env.block_env = block_env_from_header::<BlockEnvFor<FEN>>(block.header());
426 evm_env.block_env.set_number(number);
427 apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
428 evm_env,
429 block,
430 source_chain_id,
431 config,
432 );
433}
434
435pub fn deploy_contract<FEN>(
436 executor: &mut TracingExecutor<FEN>,
437 evm_env: &EvmEnvFor<FEN>,
438 tx_env: &TxEnvFor<FEN>,
439 to: TxKind,
440 chain_context: ChainFor<FEN>,
441) -> Result<Address, eyre::ErrReport>
442where
443 FEN: FoundryEvmNetwork,
444{
445 if let TxKind::Call(to) = to {
446 if to != DEFAULT_CREATE2_DEPLOYER {
447 eyre::bail!(
448 "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
449 );
450 }
451 let result = executor.transact_with_env_and_context(
452 evm_env.clone(),
453 tx_env.clone(),
454 chain_context,
455 )?;
456
457 trace!(transact_result = ?result.exit_reason);
458
459 if result.reverted {
460 let decoded_reason = if result.result.is_empty() {
461 String::new()
462 } else {
463 format!(": {}", RevertDecoder::default().decode(&result.result, result.exit_reason))
464 };
465 eyre::bail!(
466 "Failed to deploy contract via CREATE2 on fork at block{decoded_reason}.\n\
467 This typically happens when your local bytecode differs from what was actually deployed.\n\
468 Common causes:\n\
469 - Your contract source is not at the same commit used during deployment\n\
470 - Cached build artifacts are stale (try `forge clean && forge build`)\n\
471 - Compiler settings (optimizer, evm_version, via_ir) don't match the deployment"
472 );
473 }
474
475 if result.result.len() != 20 {
476 eyre::bail!(
477 "Failed to deploy contract via CREATE2 on fork at block: deployer returned {} bytes instead of 20.\n\
478 This may indicate a bytecode mismatch - ensure your source code matches the deployed contract.",
479 result.result.len()
480 );
481 }
482
483 Ok(Address::from_slice(&result.result))
484 } else {
485 let deploy_result = executor.deploy_with_env_and_context(
486 evm_env.clone(),
487 tx_env.clone(),
488 chain_context,
489 None,
490 )?;
491 trace!(deploy_result = ?deploy_result.raw.exit_reason);
492 Ok(deploy_result.address)
493 }
494}
495
496pub async fn get_runtime_codes<FEN>(
497 executor: &mut TracingExecutor<FEN>,
498 provider: &impl Provider<AnyNetwork>,
499 address: Address,
500 fork_address: Address,
501 block: Option<u64>,
502) -> Result<(Bytecode, Bytes)>
503where
504 FEN: FoundryEvmNetwork,
505{
506 let fork_runtime_code = executor
507 .backend_mut()
508 .basic(fork_address)?
509 .ok_or_else(|| {
510 eyre::eyre!(
511 "Failed to get runtime code for contract deployed on fork at address {}",
512 fork_address
513 )
514 })?
515 .code
516 .ok_or_else(|| {
517 eyre::eyre!(
518 "Bytecode does not exist for contract deployed on fork at address {}",
519 fork_address
520 )
521 })?;
522
523 let block_id = block.map_or_else(BlockId::latest, BlockId::number);
524 let onchain_runtime_code = provider.get_code_at(address).block_id(block_id).await?;
525
526 Ok((fork_runtime_code, onchain_runtime_code))
527}
528
529pub fn is_host_only(url: &Url) -> bool {
533 matches!(url.path(), "/" | "")
534}
535
536pub fn wrap_verifier_url_error(
543 err: eyre::Error,
544 verifier_url: Option<&str>,
545 using_etherscan: bool,
546) -> eyre::Error {
547 let Some(verifier_url) = verifier_url else { return err };
548 let url = match Url::parse(verifier_url) {
549 Ok(mut url) => {
550 let _ = url.set_username("");
551 let _ = url.set_password(None);
552 url.set_query(None);
553 url.set_fragment(None);
554 url
555 }
556 Err(url_err) => {
557 return err.wrap_err(format!("Invalid verifier URL provided: {url_err}"));
558 }
559 };
560 if is_host_only(&url) && using_etherscan {
561 return err.wrap_err(format!(
562 "Verifier `etherscan` requires an API endpoint, but `--verifier-url` is host-only: `{url}`.\n\
563 Fixes (pick one):\n\
564 - Append the API path, e.g. `--verifier-url {url}api`\n\
565 - Switch verifier, e.g. `--verifier sourcify` (works with host-only URLs)"
566 ));
567 }
568 err
569}
570
571pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
582 if version.build == BuildMetadata::EMPTY {
583 Ok(lookup_compiler_version(&version).await?)
584 } else {
585 Ok(version)
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use crate::verify::VerifierArgs;
593 use foundry_cli::opts::EtherscanOpts;
594 use foundry_compilers::PathStyle;
595 use foundry_config::NamedChain;
596 use foundry_test_utils::TestProject;
597
598 #[cfg(feature = "monad")]
599 fn monad_env(timestamp: u64) -> EvmEnvFor<foundry_evm::core::evm::MonadEvmNetwork> {
600 let mut env = EvmEnvFor::<foundry_evm::core::evm::MonadEvmNetwork>::default();
601 env.cfg_env.chain_id = NamedChain::Monad as u64;
602 env.block_env.set_timestamp(U256::from(timestamp));
603 env
604 }
605
606 #[test]
607 fn encoded_constructor_args_must_be_canonical() {
608 let artifact = CompactContractBytecode {
609 abi: Some(alloy_json_abi::JsonAbi::parse(["constructor(uint256 value)"]).unwrap()),
610 bytecode: None,
611 deployed_bytecode: None,
612 };
613 let args = artifact
614 .abi
615 .as_ref()
616 .unwrap()
617 .constructor()
618 .unwrap()
619 .abi_encode_input(&[DynSolValue::Uint(U256::from(1), 256)])
620 .unwrap();
621
622 assert_eq!(validate_encoded_constructor_args(&artifact, args.clone()).unwrap(), args);
623
624 let overlapping = [alloy_primitives::hex!("a1616101").as_slice(), &args].concat();
627 assert!(validate_encoded_constructor_args(&artifact, overlapping).is_err());
628 }
629
630 #[test]
631 fn typed_constructor_args_require_a_constructor() {
632 let artifact = CompactContractBytecode {
633 abi: Some(alloy_json_abi::JsonAbi::default()),
634 bytecode: None,
635 deployed_bytecode: None,
636 };
637
638 assert!(check_and_encode_args(&artifact, vec!["1".to_string()]).is_err());
639 assert_eq!(check_and_encode_args(&artifact, Vec::new()).unwrap(), Vec::<u8>::new());
640 }
641
642 #[test]
643 fn dynamic_constructor_args_are_encoded_as_top_level_params() {
644 let artifact = CompactContractBytecode {
645 abi: Some(alloy_json_abi::JsonAbi::parse(["constructor(string value)"]).unwrap()),
646 bytecode: None,
647 deployed_bytecode: None,
648 };
649
650 let encoded = check_and_encode_args(&artifact, vec!["hi".to_string()]).unwrap();
651
652 let expected = alloy_primitives::hex!(
655 "0000000000000000000000000000000000000000000000000000000000000020"
656 "0000000000000000000000000000000000000000000000000000000000000002"
657 "6869000000000000000000000000000000000000000000000000000000000000"
658 );
659 assert_eq!(encoded, expected);
660 }
661
662 #[test]
663 fn creation_code_must_end_with_constructor_args() {
664 let code = alloy_primitives::hex!("6080604052348015600e575f5ffd5b50607b80601a5f395ff3fe");
665 let real_args = [0x11u8; 32];
666 let wrong_args = [0x22u8; 32];
667
668 let onchain = [code.as_slice(), &real_args].concat();
669
670 let local = [code.as_slice(), &wrong_args].concat();
673 assert_eq!(match_bytecodes(&local, &onchain, &wrong_args, false, BytecodeHash::Ipfs), None);
674
675 let local = [code.as_slice(), &real_args].concat();
676 assert_eq!(
677 match_bytecodes(&local, &onchain, &real_args, false, BytecodeHash::Ipfs),
678 Some(VerificationType::Full)
679 );
680
681 let suffix_args =
684 DynSolValue::Tuple(vec![DynSolValue::Bytes(real_args.to_vec())]).abi_encode();
685 let deployment_args =
686 DynSolValue::Tuple(vec![DynSolValue::Bytes(suffix_args.clone())]).abi_encode();
687 assert!(deployment_args.ends_with(&suffix_args));
688
689 let onchain = [code.as_slice(), deployment_args.as_slice()].concat();
690 let local = [code.as_slice(), suffix_args.as_slice()].concat();
691 assert_eq!(
692 match_bytecodes(&local, &onchain, &suffix_args, false, BytecodeHash::Ipfs),
693 None
694 );
695 }
696
697 #[test]
698 fn build_project_finds_artifact_by_relative_contract_path() {
699 let prj = TestProject::new("verify-bytecode-relative-path", PathStyle::Dapptools);
700 prj.add_source(
701 "Counter.sol",
702 r#"
703pragma solidity 0.8.16;
704
705contract Counter {
706 uint256 public number;
707}
708"#,
709 );
710 prj.add_source(
711 "Broken.sol",
712 r#"
713pragma solidity 0.8.16;
714
715contract Broken {
716 this is not valid Solidity
717}
718"#,
719 );
720
721 let mut config = Config::load_with_root(prj.root()).unwrap();
722 config.solc = Some("0.8.16".into());
723 let args = VerifyBytecodeArgs {
724 address: Address::ZERO,
725 contract: "src/Counter.sol:Counter".parse().unwrap(),
726 block: None,
727 constructor_args: None,
728 encoded_constructor_args: None,
729 constructor_args_path: None,
730 rpc_url: None,
731 network: None,
732 etherscan: EtherscanOpts::default(),
733 verifier: VerifierArgs::default(),
734 libraries: Vec::new(),
735 root: Some(prj.root().to_path_buf()),
736 ignore: None,
737 };
738
739 let artifact = build_project(&args, &config).unwrap();
740
741 assert!(artifact.bytecode.and_then(|bytecode| bytecode.into_bytes()).is_some());
742 }
743
744 #[test]
745 fn load_fork_config_and_evm_opts_serializes_chain_as_id() {
746 let config = Config { chain: Some(NamedChain::Mainnet.into()), ..Default::default() };
747
748 let (fork_config, evm_opts) = load_fork_config_and_evm_opts(&config).unwrap();
749
750 assert_eq!(fork_config.chain, Some(NamedChain::Mainnet.into()));
751 assert_eq!(evm_opts.env.chain_id, Some(1));
752 }
753
754 #[test]
755 #[cfg(feature = "monad")]
756 fn runtime_spec_uses_monad_source_chain_timestamp() {
757 let monad_nine_timestamp = foundry_evm::hardforks::MonadHardfork::MonadNine
758 .mainnet_activation_timestamp()
759 .unwrap();
760
761 let before_config = Config {
762 evm_version: foundry_compilers::artifacts::EvmVersion::Osaka,
763 ..Default::default()
764 };
765 let mut before_env = monad_env(monad_nine_timestamp - 1);
766 before_env.cfg_env.chain_id = NamedChain::Mainnet as u64;
767 let before = resolve_runtime_spec::<foundry_evm::core::evm::MonadEvmNetwork>(
768 &before_config,
769 NamedChain::Monad as u64,
770 None,
771 &mut before_env,
772 );
773
774 assert_eq!(
775 before,
776 Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadEight))
777 );
778 assert_eq!(before_env.cfg_env.spec, foundry_evm::hardforks::MonadHardfork::MonadEight);
779 assert_eq!(before_env.cfg_env.chain_id, NamedChain::Mainnet as u64);
780
781 let after_config = Config {
782 evm_version: foundry_compilers::artifacts::EvmVersion::Prague,
783 ..Default::default()
784 };
785 let mut after_env = monad_env(monad_nine_timestamp);
786 let after = resolve_runtime_spec::<foundry_evm::core::evm::MonadEvmNetwork>(
787 &after_config,
788 NamedChain::Monad as u64,
789 None,
790 &mut after_env,
791 );
792
793 assert_eq!(
794 after,
795 Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadNine))
796 );
797 assert_eq!(after_env.cfg_env.spec, foundry_evm::hardforks::MonadHardfork::MonadNine);
798 }
799
800 #[test]
801 #[cfg(feature = "monad")]
802 fn runtime_spec_and_labels_prefer_explicit_monad_hardfork() {
803 let mut config = Config {
804 hardfork: Some(foundry_evm::hardforks::MonadHardfork::MonadEight.into()),
805 ..Default::default()
806 };
807 let mut env = monad_env(
808 foundry_evm::hardforks::MonadHardfork::MonadNine
809 .mainnet_activation_timestamp()
810 .unwrap(),
811 );
812 let networks = NetworkConfigs::with_monad();
813
814 let resolved = resolve_runtime_spec::<foundry_evm::core::evm::MonadEvmNetwork>(
815 &config,
816 NamedChain::Monad as u64,
817 Some(foundry_evm::hardforks::MonadHardfork::MonadNine.into()),
818 &mut env,
819 );
820 TracingExecutor::<foundry_evm::core::evm::MonadEvmNetwork>::extend_precompile_labels(
821 &mut config,
822 networks,
823 resolved,
824 );
825
826 assert_eq!(
827 resolved,
828 Some(FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadEight))
829 );
830 assert_eq!(env.cfg_env.spec, foundry_evm::hardforks::MonadHardfork::MonadEight);
831 assert!(config.labels.values().any(|label| label == "Staking"));
832 assert!(!config.labels.values().any(|label| label == "ReserveBalance"));
833 }
834
835 #[test]
836 fn test_host_only() {
837 assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
838 assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
839 assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
840 }
841
842 #[test]
843 fn wrap_verifier_url_error_passes_through_when_no_url() {
844 let err = eyre::eyre!("upstream failure");
845 let wrapped = wrap_verifier_url_error(err, None, true);
846 assert_eq!(wrapped.to_string(), "upstream failure");
847 }
848
849 #[test]
850 fn wrap_verifier_url_error_adds_hint_for_host_only_etherscan_url() {
851 let err = eyre::eyre!("upstream failure");
852 let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), true);
853 let msg = format!("{wrapped:#}");
854 assert!(msg.contains("host-only"), "message: {msg}");
855 assert!(msg.contains("--verifier-url https://contracts.tempo.xyz/api"), "message: {msg}");
856 assert!(msg.contains("--verifier sourcify"), "message: {msg}");
857 }
858
859 #[test]
862 fn wrap_verifier_url_error_does_not_hint_for_non_etherscan_provider() {
863 let err = eyre::eyre!("upstream failure");
864 let wrapped = wrap_verifier_url_error(err, Some("https://contracts.tempo.xyz"), false);
865 assert_eq!(wrapped.to_string(), "upstream failure");
866 }
867
868 #[test]
869 fn wrap_verifier_url_error_reports_invalid_url() {
870 let err = eyre::eyre!("upstream failure");
871 let wrapped = wrap_verifier_url_error(err, Some("not a url"), true);
872 let msg = format!("{wrapped:#}");
873 assert!(msg.contains("Invalid verifier URL"), "message: {msg}");
874 assert!(!msg.contains("not a url"), "message: {msg}");
875 }
876
877 #[test]
878 fn wrap_verifier_url_error_redacts_credentials_and_query() {
879 let err = eyre::eyre!("upstream failure");
880 let wrapped = wrap_verifier_url_error(
881 err,
882 Some("https://user:secret@example.com?api_key=secret"),
883 true,
884 );
885 let msg = format!("{wrapped:#}");
886 assert!(msg.contains("https://example.com/"));
887 assert!(!msg.contains("user"));
888 assert!(!msg.contains("secret"));
889 assert!(!msg.contains("api_key"));
890 }
891}