Skip to main content

forge_verify/etherscan/
mod.rs

1use crate::{
2    VerifierArgs,
3    provider::{
4        ExternalVerificationContext, VerificationContext, VerificationProvider,
5        VerificationProviderType,
6    },
7    utils::ensure_solc_build_metadata,
8    verify::{ContractLanguage, VerifyArgs, VerifyCheckArgs},
9};
10use alloy_json_abi::Function;
11use alloy_primitives::hex;
12use alloy_provider::Provider;
13use alloy_rpc_types::TransactionTrait;
14use eyre::{Context, OptionExt, Result, eyre};
15use foundry_block_explorers::{
16    Client,
17    errors::EtherscanError,
18    verify::{CodeFormat, VerifyContract},
19};
20use foundry_cli::{
21    opts::EtherscanOpts,
22    utils::{LoadConfig, get_provider, read_constructor_args_file},
23};
24use foundry_common::{abi::encode_function_args, retry::RetryError};
25use foundry_compilers::{Artifact, artifacts::BytecodeObject};
26use foundry_config::Config;
27use foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER;
28use regex::Regex;
29use semver::BuildMetadata;
30use std::{fmt::Debug, future::Future as StdFuture, pin::Pin, sync::LazyLock, time::Duration};
31
32mod flatten;
33
34mod standard_json;
35
36pub static RE_BUILD_COMMIT: LazyLock<Regex> =
37    LazyLock::new(|| Regex::new(r"(?P<commit>commit\.[0-9,a-f]{8})").unwrap());
38
39#[derive(Clone, Debug, Default)]
40#[non_exhaustive]
41pub struct EtherscanVerificationProvider;
42
43/// The contract source provider for [EtherscanVerificationProvider]
44///
45/// Returns source, contract_name and the source [CodeFormat]
46trait EtherscanSourceProvider: Send + Sync + Debug {
47    fn source(
48        &self,
49        args: &VerifyArgs,
50        context: &VerificationContext,
51    ) -> Result<(String, String, CodeFormat)>;
52}
53
54#[async_trait::async_trait]
55impl VerificationProvider for EtherscanVerificationProvider {
56    fn provider_type(&self) -> VerificationProviderType {
57        VerificationProviderType::Etherscan
58    }
59
60    async fn preflight_verify_check(
61        &mut self,
62        args: VerifyArgs,
63        context: VerificationContext,
64    ) -> Result<()> {
65        let _ = self.prepare_verify_request(&args, &context).await?;
66        Ok(())
67    }
68
69    async fn submit(
70        &mut self,
71        args: VerifyArgs,
72        context: VerificationContext,
73    ) -> Result<Option<VerifyCheckArgs>> {
74        let (etherscan, verify_args) = self.prepare_verify_request(&args, &context).await?;
75        self.submit_verify_request(args, etherscan, verify_args).await
76    }
77
78    fn submit_external(
79        &mut self,
80        args: VerifyArgs,
81        context: ExternalVerificationContext,
82    ) -> Pin<Box<dyn StdFuture<Output = Result<Option<VerifyCheckArgs>>> + '_>> {
83        Box::pin(async move {
84            let etherscan = self.client(&args.etherscan, &args.verifier, &context.config)?;
85            let verify_args = self.create_external_verify_request(&args, &context)?;
86            self.submit_verify_request(args, etherscan, verify_args).await
87        })
88    }
89
90    /// Executes the command to check verification status on Etherscan
91    async fn check(&self, args: VerifyCheckArgs) -> Result<()> {
92        let config = args.load_config()?;
93        let etherscan = self.client(&args.etherscan, &args.verifier, &config)?;
94        args.retry
95            .into_retry()
96            .run_async_until_break(|| async {
97                let resp = etherscan
98                    .check_contract_verification_status(args.id.clone())
99                    .await
100                    .wrap_err("Failed to request verification status")
101                    .map_err(RetryError::Retry)?;
102
103                trace!(status = %resp.status, "Received Etherscan verification response");
104
105                let _ = sh_status!(
106                    "Contract verification status:\nResponse: `{}`\nDetails: `{}`",
107                    sanitize_remote_message(&resp.message),
108                    sanitize_remote_message(&resp.result)
109                );
110
111                if resp.result == "Pending in queue"
112                    || resp.result.starts_with("Error: contract does not exist")
113                {
114                    return Err(RetryError::Retry(eyre!("Verification is still pending...")));
115                }
116
117                if resp.result == "Unable to verify" {
118                    return Err(RetryError::Retry(eyre!("Unable to verify.")));
119                }
120
121                if resp.result == "Already Verified" {
122                    let _ = sh_status!("Contract source code already verified");
123                    return Ok(());
124                }
125
126                if resp.status == "0" {
127                    return Err(RetryError::Break(eyre!(
128                        "Contract verification failed:\nStatus: `{}`\nResult: `{}`",
129                        resp.status,
130                        sanitize_remote_message(&resp.result)
131                    )));
132                }
133
134                if resp.result == "Pass - Verified" {
135                    let _ = sh_status!("Contract successfully verified");
136                }
137
138                Ok(())
139            })
140            .await
141            .wrap_err("Checking verification result failed")
142    }
143}
144
145impl EtherscanVerificationProvider {
146    async fn submit_verify_request(
147        &self,
148        args: VerifyArgs,
149        etherscan: Client,
150        verify_args: VerifyContract,
151    ) -> Result<Option<VerifyCheckArgs>> {
152        if !args.skip_is_verified_check
153            && self.is_contract_verified(&etherscan, &verify_args).await?
154        {
155            sh_status!(
156                "Contract [{}] {:?} is already verified. Skipping verification.",
157                verify_args.contract_name,
158                verify_args.address.to_checksum(None)
159            )?;
160
161            return Ok(None);
162        }
163
164        trace!(
165            provider = "Etherscan",
166            address = %verify_args.address,
167            target = %verify_args.contract_name,
168            "submitting verification request"
169        );
170
171        let resp = args
172            .retry
173            .into_retry()
174            .run_async(|| async {
175                sh_status!(
176                    "Submitting verification for [{}] {}.",
177                    verify_args.contract_name,
178                    verify_args.address
179                )?;
180                let resp = etherscan
181                    .submit_contract_verification(&verify_args)
182                    .await
183                    .wrap_err("Failed to submit Etherscan contract verification")?;
184
185                trace!(status = %resp.status, "Received Etherscan verification response");
186
187                if resp.status == "0" {
188                    if resp.result == "Contract source code already verified"
189                        // specific for blockscout response
190                        || resp.result == "Smart-contract already verified."
191                    {
192                        return Ok(None);
193                    }
194
195                    if resp.result.starts_with("Unable to locate ContractCode at")
196                        || resp.result.starts_with("The address is not a smart contract")
197                        || resp.result.starts_with("Address is not a smart-contract")
198                    {
199                        warn!("{}", sanitize_remote_message(&resp.result));
200                        return Err(eyre!(
201                            "Could not detect deployment: {}",
202                            sanitize_remote_message(&resp.result)
203                        ));
204                    }
205
206                    warn!(status = %resp.status, "Etherscan verification submission failed");
207                    eyre::bail!(
208                        "Encountered an error verifying this contract:\nResponse: `{}`\nDetails: `{}`",
209                        sanitize_remote_message(&resp.message),
210                        sanitize_remote_message(&resp.result)
211                    );
212                }
213
214                Ok(Some(resp))
215            })
216            .await?;
217
218        if let Some(resp) = resp {
219            let url = etherscan.address_url(args.address);
220            let display_id = sanitize_remote_message(&resp.result);
221            sh_status!(
222                "Submitted contract for verification:\n\tResponse: `{}`\n\tGUID: `{}`\n\tURL: {}",
223                sanitize_remote_message(&resp.message),
224                display_id,
225                url
226            )?;
227            if args.print_submission_result_to_stdout {
228                sh_println!("{}\t{}", display_id, url)?;
229            }
230            Ok(Some(VerifyCheckArgs {
231                id: resp.result,
232                etherscan: args.etherscan,
233                retry: args.retry,
234                verifier: args.verifier,
235            }))
236        } else {
237            sh_status!("Contract source code already verified")?;
238            Ok(None)
239        }
240    }
241
242    fn create_external_verify_request(
243        &self,
244        args: &VerifyArgs,
245        context: &ExternalVerificationContext,
246    ) -> Result<VerifyContract> {
247        let source = serde_json::to_string(&context.standard_json_input)
248            .wrap_err("Failed to serialize standard json input")?;
249        let compiler_version = format!("v{}", context.compiler_version);
250        let mut request =
251            VerifyContract::new(args.address, context.target.clone(), source, compiler_version)
252                .constructor_arguments(args.constructor_args.clone())
253                .code_format(CodeFormat::StandardJsonInput);
254        apply_license_type(&mut request, args.license_type.as_deref());
255        Ok(request)
256    }
257
258    /// Create a source provider
259    fn source_provider(&self, args: &VerifyArgs) -> Box<dyn EtherscanSourceProvider> {
260        if args.flatten {
261            Box::new(flatten::EtherscanFlattenedSource)
262        } else {
263            Box::new(standard_json::EtherscanStandardJsonSource)
264        }
265    }
266
267    /// Configures the API request to the Etherscan API using the given [`VerifyArgs`].
268    async fn prepare_verify_request(
269        &mut self,
270        args: &VerifyArgs,
271        context: &VerificationContext,
272    ) -> Result<(Client, VerifyContract)> {
273        let config = args.load_config()?;
274        let etherscan = self.client(&args.etherscan, &args.verifier, &config)?;
275        let verify_args = self.create_verify_request(args, context).await?;
276
277        Ok((etherscan, verify_args))
278    }
279
280    /// Queries the Etherscan API to verify if the contract is already verified.
281    async fn is_contract_verified(
282        &self,
283        etherscan: &Client,
284        verify_contract: &VerifyContract,
285    ) -> Result<bool> {
286        let check = etherscan.contract_abi(verify_contract.address).await;
287
288        if let Err(err) = check {
289            return match err {
290                EtherscanError::ContractCodeNotVerified(_) => Ok(false),
291                error => Err(error).wrap_err_with(|| {
292                    format!("Failed to obtain contract ABI for {}", verify_contract.address)
293                }),
294            };
295        }
296
297        Ok(true)
298    }
299
300    /// Create an Etherscan client.
301    pub(crate) fn client(
302        &self,
303        etherscan_opts: &EtherscanOpts,
304        verifier_args: &VerifierArgs,
305        config: &Config,
306    ) -> Result<Client> {
307        let chain = etherscan_opts.chain.unwrap_or_default();
308        let etherscan_key = etherscan_opts.key();
309        let verifier_type = verifier_args.effective_type();
310        let verifier_url = verifier_args.verifier_url.as_deref();
311
312        // Verifier is etherscan if explicitly set or if no verifier set (default sourcify) but
313        // API key passed.
314        let is_etherscan = verifier_type.is_etherscan()
315            || (verifier_type.is_sourcify() && etherscan_key.is_some());
316        let etherscan_config = match config.get_etherscan_config_with_chain(Some(chain)) {
317            Ok(config) => config,
318            // The selected URL is authoritative, so an invalid lower-priority endpoint must not
319            // prevent creating the client.
320            Err(_) if verifier_url.is_some() => None,
321            Err(err) => return Err(err.into()),
322        };
323
324        let api_url =
325            verifier_url.or_else(|| etherscan_config.as_ref().map(|c| c.api_url.as_str()));
326        let base_url = etherscan_config
327            .as_ref()
328            .and_then(|c| c.browser_url.as_deref())
329            .or_else(|| chain.etherscan_urls().map(|(_, url)| url));
330        let etherscan_key =
331            etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.clone()));
332
333        let mut builder = Client::builder();
334
335        builder = if let Some(api_url) = api_url {
336            // we don't want any trailing slashes because this can cause cloudflare issues: <https://github.com/foundry-rs/foundry/pull/6079>
337            let api_url = api_url.trim_end_matches('/');
338            let base_url = if is_etherscan {
339                base_url.unwrap_or(api_url)
340            } else {
341                // If verifier is not Etherscan then set base url as api url without /api suffix.
342                api_url.strip_suffix("/api").unwrap_or(api_url)
343            };
344            builder.with_api_url(api_url)?.with_url(base_url)?
345        } else {
346            builder.chain(chain)?
347        };
348
349        let http = reqwest::Client::builder()
350            .timeout(Duration::from_secs(30))
351            .connect_timeout(Duration::from_secs(10))
352            .build()
353            .wrap_err("Failed to create Etherscan HTTP client")?;
354        builder
355            .with_client(http)
356            .with_api_key(etherscan_key.unwrap_or_default())
357            .build()
358            .wrap_err("Failed to create Etherscan client")
359    }
360
361    /// Creates the `VerifyContract` Etherscan request in order to verify the contract
362    ///
363    /// If `--flatten` is set to `true` then this will send with [`CodeFormat::SingleFile`]
364    /// otherwise this will use the [`CodeFormat::StandardJsonInput`]
365    pub async fn create_verify_request(
366        &mut self,
367        args: &VerifyArgs,
368        context: &VerificationContext,
369    ) -> Result<VerifyContract> {
370        let (source, contract_name, code_format) =
371            self.source_provider(args).source(args, context)?;
372
373        let lang = args.detect_language(context);
374
375        let mut compiler_version = context.compiler_version.clone();
376        compiler_version.build = match RE_BUILD_COMMIT.captures(compiler_version.build.as_str()) {
377            Some(cap) => BuildMetadata::new(cap.name("commit").unwrap().as_str())?,
378            _ => BuildMetadata::EMPTY,
379        };
380
381        let compiler_version = if matches!(lang, ContractLanguage::Vyper) {
382            format!("vyper:{}", compiler_version.to_string().split('+').next().unwrap_or("0.0.0"))
383        } else {
384            format!("v{}", ensure_solc_build_metadata(context.compiler_version.clone()).await?)
385        };
386
387        let constructor_args = self.constructor_args(args, context).await?;
388        let mut verify_args =
389            VerifyContract::new(args.address, contract_name, source, compiler_version)
390                .constructor_arguments(constructor_args)
391                .code_format(code_format);
392
393        if args.via_ir {
394            // we explicitly set this __undocumented__ argument to true if provided by the user,
395            // though this info is also available in the compiler settings of the standard json
396            // object if standard json is used
397            // unclear how Etherscan interprets this field in standard-json mode
398            verify_args = verify_args.via_ir(true);
399        }
400
401        apply_license_type(&mut verify_args, args.license_type.as_deref());
402
403        if code_format == CodeFormat::SingleFile {
404            verify_args = if let Some(optimizations) = args.num_of_optimizations {
405                verify_args.optimized().runs(optimizations as u32)
406            } else if context.config.optimizer == Some(true) {
407                verify_args
408                    .optimized()
409                    .runs(context.config.optimizer_runs.unwrap_or(200).try_into()?)
410            } else {
411                verify_args.not_optimized()
412            };
413        }
414
415        if code_format == CodeFormat::VyperJson {
416            verify_args =
417                if args.num_of_optimizations.is_some() || context.config.optimizer == Some(true) {
418                    verify_args.optimized().runs(1)
419                } else {
420                    verify_args.not_optimized().runs(0)
421                }
422        }
423
424        Ok(verify_args)
425    }
426
427    /// Return the optional encoded constructor arguments. If the path to
428    /// constructor arguments was provided, read them and encode. Otherwise,
429    /// return whatever was set in the [VerifyArgs] args.
430    async fn constructor_args(
431        &mut self,
432        args: &VerifyArgs,
433        context: &VerificationContext,
434    ) -> Result<Option<String>> {
435        if let Some(ref constructor_args_path) = args.constructor_args_path {
436            let abi = context.get_target_abi()?;
437            let constructor = abi
438                .constructor()
439                .ok_or_else(|| eyre!("Can't retrieve constructor info from artifact ABI."))?;
440            let func = Function {
441                name: "constructor".to_string(),
442                inputs: constructor.inputs.clone(),
443                outputs: vec![],
444                state_mutability: alloy_json_abi::StateMutability::NonPayable,
445            };
446            let encoded_args = encode_function_args(
447                &func,
448                read_constructor_args_file(constructor_args_path.clone())?,
449            )?;
450            let encoded_args = hex::encode(encoded_args);
451            return Ok(Some(encoded_args[8..].into()));
452        }
453        if args.guess_constructor_args {
454            return Ok(Some(self.guess_constructor_args(args, context).await?));
455        }
456
457        Ok(args.constructor_args.clone())
458    }
459
460    /// Uses Etherscan API to fetch contract creation transaction.
461    /// If transaction is a create transaction or a invocation of default CREATE2 deployer, tries to
462    /// match provided creation code with local bytecode of the target contract.
463    /// If bytecode match, returns latest bytes of on-chain creation code as constructor arguments.
464    async fn guess_constructor_args(
465        &mut self,
466        args: &VerifyArgs,
467        context: &VerificationContext,
468    ) -> Result<String> {
469        let provider = get_provider(&context.config)?;
470        let client = self.client(&args.etherscan, &args.verifier, &context.config)?;
471
472        let creation_data = client.contract_creation_data(args.address).await?;
473        let transaction = provider
474            .get_transaction_by_hash(creation_data.transaction_hash)
475            .await?
476            .ok_or_eyre("Transaction not found")?;
477        let receipt = provider
478            .get_transaction_receipt(creation_data.transaction_hash)
479            .await?
480            .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?;
481
482        let maybe_creation_code = if receipt.contract_address == Some(args.address) {
483            transaction.input()
484        } else if transaction.to() == Some(DEFAULT_CREATE2_DEPLOYER) {
485            &transaction.input()[32..]
486        } else {
487            eyre::bail!(
488                "Fetching of constructor arguments is not supported for contracts created by contracts"
489            );
490        };
491
492        let output = context.project.compile_file(&context.target_path)?;
493        let artifact = output
494            .find(&context.target_path, &context.target_name)
495            .ok_or_eyre("Contract artifact wasn't found locally")?;
496        let bytecode = artifact
497            .get_bytecode_object()
498            .ok_or_eyre("Contract artifact does not contain bytecode")?;
499
500        let bytecode = match bytecode.as_ref() {
501            BytecodeObject::Bytecode(bytes) => Ok(bytes),
502            BytecodeObject::Unlinked(_) => {
503                Err(eyre!("You have to provide correct libraries to use --guess-constructor-args"))
504            }
505        }?;
506
507        if maybe_creation_code.starts_with(bytecode) {
508            let constructor_args = &maybe_creation_code[bytecode.len()..];
509            let constructor_args = hex::encode(constructor_args);
510            sh_status!("Identified constructor arguments: {constructor_args}")?;
511            Ok(constructor_args)
512        } else {
513            eyre::bail!("Local bytecode doesn't match on-chain bytecode");
514        }
515    }
516}
517
518fn sanitize_remote_message(message: &str) -> String {
519    message.chars().map(|ch| if ch.is_control() { ' ' } else { ch }).take(512).collect()
520}
521
522fn apply_license_type(verify_args: &mut VerifyContract, license_type: Option<&str>) {
523    if let Some(license_type) = license_type {
524        verify_args.other.insert("licenseType".to_string(), license_type.to_string());
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use clap::Parser;
532    use foundry_common::fs;
533    use foundry_test_utils::{forgetest_async, str};
534    use tempfile::tempdir;
535
536    #[test]
537    fn external_request_preserves_raw_input_and_target() {
538        let args =
539            VerifyArgs::parse_from(["foundry-cli", "0xd8509bee9c9bf012282ad33aba0d87241baf5064"]);
540        let input = serde_json::json!({
541            "language": "Solidity",
542            "settings": { "unknownSetting": { "futureField": true } },
543            "unknownTopLevel": [1, 2, 3]
544        });
545        let context = ExternalVerificationContext {
546            config: Config::default(),
547            compiler_version: "0.8.30+commit.73712a01".parse().unwrap(),
548            standard_json_input: std::sync::Arc::new(input.clone()),
549            target: "contracts/Unknown.sol:ExactTarget".to_string(),
550        };
551
552        let request =
553            EtherscanVerificationProvider.create_external_verify_request(&args, &context).unwrap();
554        assert_eq!(serde_json::from_str::<serde_json::Value>(&request.source).unwrap(), input);
555        assert_eq!(request.contract_name, "contracts/Unknown.sol:ExactTarget");
556        assert_eq!(request.compiler_version, "v0.8.30+commit.73712a01");
557    }
558
559    #[test]
560    fn applies_license_type_to_verify_request() {
561        let mut verify_args = VerifyContract::new(
562            Default::default(),
563            "Counter".to_string(),
564            "contract Counter {}".to_string(),
565            "v0.8.23+commit.f704f362".to_string(),
566        );
567
568        apply_license_type(&mut verify_args, Some("13"));
569
570        assert_eq!(verify_args.other.get("licenseType").map(String::as_str), Some("13"));
571    }
572
573    #[test]
574    fn can_extract_etherscan_verify_config() {
575        let temp = tempdir().unwrap();
576        let root = temp.path();
577
578        let config = r#"
579                [profile.default]
580
581                [etherscan]
582                amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
583            "#;
584
585        let toml_file = root.join(Config::FILE_NAME);
586        fs::write(toml_file, config).unwrap();
587
588        let args: VerifyArgs = VerifyArgs::parse_from([
589            "foundry-cli",
590            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
591            "src/Counter.sol:Counter",
592            "--chain",
593            "amoy",
594            "--root",
595            root.as_os_str().to_str().unwrap(),
596        ]);
597
598        let config = args.load_config().unwrap();
599
600        let etherscan = EtherscanVerificationProvider::default();
601        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
602        // Custom URL from foundry.toml should be used
603        assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
604
605        assert!(format!("{client:?}").contains("dummykey"));
606
607        let args: VerifyArgs = VerifyArgs::parse_from([
608            "foundry-cli",
609            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
610            "src/Counter.sol:Counter",
611            "--chain",
612            "amoy",
613            "--verifier-url",
614            "https://verifier-url.com/",
615            "--root",
616            root.as_os_str().to_str().unwrap(),
617        ]);
618
619        let config = args.load_config().unwrap();
620
621        let etherscan = EtherscanVerificationProvider::default();
622        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
623        assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
624        assert!(format!("{client:?}").contains("dummykey"));
625    }
626
627    #[test]
628    fn can_extract_etherscan_v2_verify_config() {
629        let temp = tempdir().unwrap();
630        let root = temp.path();
631
632        let config = r#"
633                [profile.default]
634
635                [etherscan]
636                amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
637            "#;
638
639        let toml_file = root.join(Config::FILE_NAME);
640        fs::write(toml_file, config).unwrap();
641
642        let args: VerifyArgs = VerifyArgs::parse_from([
643            "foundry-cli",
644            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
645            "src/Counter.sol:Counter",
646            "--verifier",
647            "etherscan",
648            "--chain",
649            "amoy",
650            "--root",
651            root.as_os_str().to_str().unwrap(),
652        ]);
653
654        let config = args.load_config().unwrap();
655
656        let etherscan = EtherscanVerificationProvider::default();
657
658        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
659
660        // Custom URL from foundry.toml should be used
661        assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
662        assert!(format!("{client:?}").contains("dummykey"));
663
664        let args: VerifyArgs = VerifyArgs::parse_from([
665            "foundry-cli",
666            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
667            "src/Counter.sol:Counter",
668            "--verifier",
669            "etherscan",
670            "--chain",
671            "amoy",
672            "--verifier-url",
673            "https://verifier-url.com/",
674            "--root",
675            root.as_os_str().to_str().unwrap(),
676        ]);
677
678        let config = args.load_config().unwrap();
679
680        assert_eq!(args.verifier.effective_type(), VerificationProviderType::Etherscan);
681
682        let etherscan = EtherscanVerificationProvider::default();
683        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
684        assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
685        assert!(format!("{client:?}").contains("dummykey"));
686    }
687
688    #[tokio::test(flavor = "multi_thread")]
689    async fn fails_on_disabled_cache_and_missing_info() {
690        let temp = tempdir().unwrap();
691        let root = temp.path();
692        let root_path = root.as_os_str().to_str().unwrap();
693
694        let config = r"
695                [profile.default]
696                cache = false
697            ";
698
699        let toml_file = root.join(Config::FILE_NAME);
700        fs::write(toml_file, config).unwrap();
701
702        let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064";
703        let contract_name = "Counter";
704        let src_dir = "src";
705        fs::create_dir_all(root.join(src_dir)).unwrap();
706        let contract_path = format!("{src_dir}/Counter.sol");
707        fs::write(root.join(&contract_path), "").unwrap();
708
709        // No compiler argument
710        let args = VerifyArgs::parse_from([
711            "foundry-cli",
712            address,
713            &format!("{contract_path}:{contract_name}"),
714            "--root",
715            root_path,
716        ]);
717        let result = args.resolve_context().await;
718        assert!(result.is_err());
719        assert_eq!(
720            result.unwrap_err().to_string(),
721            "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
722        );
723    }
724
725    forgetest_async!(respects_path_for_duplicate, |prj, cmd| {
726        prj.add_source("Counter1", "contract Counter {}");
727        prj.add_source("Counter2", "contract Counter {}");
728
729        cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#"
730[COMPILING_FILES] with [SOLC_VERSION]
731...
732[SOLC_VERSION] [ELAPSED]
733Compiler run successful!
734
735"#]]);
736
737        let args = VerifyArgs::parse_from([
738            "foundry-cli",
739            "0x0000000000000000000000000000000000000000",
740            "src/Counter1.sol:Counter",
741            "--root",
742            &prj.root().to_string_lossy(),
743        ]);
744        let context = args.resolve_context().await.unwrap();
745
746        let mut etherscan = EtherscanVerificationProvider::default();
747        etherscan.preflight_verify_check(args, context).await.unwrap();
748    });
749}