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