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_dyn_abi::JsonAbiExt;
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_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(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 values = encode_args(
441                &constructor.inputs,
442                read_constructor_args_file(constructor_args_path.clone())?,
443            )?;
444            return Ok(Some(hex::encode(constructor.abi_encode_input(&values)?)));
445        }
446        if args.guess_constructor_args {
447            return Ok(Some(self.guess_constructor_args(args, context).await?));
448        }
449
450        Ok(args.constructor_args.clone())
451    }
452
453    /// Uses Etherscan API to fetch contract creation transaction.
454    /// If transaction is a create transaction or a invocation of default CREATE2 deployer, tries to
455    /// match provided creation code with local bytecode of the target contract.
456    /// If bytecode match, returns latest bytes of on-chain creation code as constructor arguments.
457    async fn guess_constructor_args(
458        &mut self,
459        args: &VerifyArgs,
460        context: &VerificationContext,
461    ) -> Result<String> {
462        let provider = get_provider(&context.config)?;
463        let client = self.client(&args.etherscan, &args.verifier, &context.config)?;
464
465        let creation_data = client.contract_creation_data(args.address).await?;
466        let transaction = provider
467            .get_transaction_by_hash(creation_data.transaction_hash)
468            .await?
469            .ok_or_eyre("Transaction not found")?;
470        let receipt = provider
471            .get_transaction_receipt(creation_data.transaction_hash)
472            .await?
473            .ok_or_eyre("Couldn't fetch transaction receipt from RPC")?;
474
475        let maybe_creation_code = if receipt.contract_address == Some(args.address) {
476            transaction.input()
477        } else if transaction.to() == Some(DEFAULT_CREATE2_DEPLOYER) {
478            &transaction.input()[32..]
479        } else {
480            eyre::bail!(
481                "Fetching of constructor arguments is not supported for contracts created by contracts"
482            );
483        };
484
485        let output = context.project.compile_file(&context.target_path)?;
486        let artifact = output
487            .find(&context.target_path, &context.target_name)
488            .ok_or_eyre("Contract artifact wasn't found locally")?;
489        let bytecode = artifact
490            .get_bytecode_object()
491            .ok_or_eyre("Contract artifact does not contain bytecode")?;
492
493        let bytecode = match bytecode.as_ref() {
494            BytecodeObject::Bytecode(bytes) => Ok(bytes),
495            BytecodeObject::Unlinked(_) => {
496                Err(eyre!("You have to provide correct libraries to use --guess-constructor-args"))
497            }
498        }?;
499
500        if maybe_creation_code.starts_with(bytecode) {
501            let constructor_args = &maybe_creation_code[bytecode.len()..];
502            let constructor_args = hex::encode(constructor_args);
503            sh_status!("Identified constructor arguments: {constructor_args}")?;
504            Ok(constructor_args)
505        } else {
506            eyre::bail!("Local bytecode doesn't match on-chain bytecode");
507        }
508    }
509}
510
511fn sanitize_remote_message(message: &str) -> String {
512    message.chars().map(|ch| if ch.is_control() { ' ' } else { ch }).take(512).collect()
513}
514
515fn apply_license_type(verify_args: &mut VerifyContract, license_type: Option<&str>) {
516    if let Some(license_type) = license_type {
517        verify_args.other.insert("licenseType".to_string(), license_type.to_string());
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use clap::Parser;
525    use foundry_common::fs;
526    use foundry_test_utils::{forgetest, str, util::SOLC_VERSION};
527    use tempfile::tempdir;
528
529    #[test]
530    fn external_request_preserves_raw_input_and_target() {
531        let args =
532            VerifyArgs::parse_from(["foundry-cli", "0xd8509bee9c9bf012282ad33aba0d87241baf5064"]);
533        let input = serde_json::json!({
534            "language": "Solidity",
535            "settings": { "unknownSetting": { "futureField": true } },
536            "unknownTopLevel": [1, 2, 3]
537        });
538        let context = ExternalVerificationContext {
539            config: Config::default(),
540            compiler_version: "0.8.30+commit.73712a01".parse().unwrap(),
541            standard_json_input: std::sync::Arc::new(input.clone()),
542            target: "contracts/Unknown.sol:ExactTarget".to_string(),
543        };
544
545        let request =
546            EtherscanVerificationProvider.create_external_verify_request(&args, &context).unwrap();
547        assert_eq!(serde_json::from_str::<serde_json::Value>(&request.source).unwrap(), input);
548        assert_eq!(request.contract_name, "contracts/Unknown.sol:ExactTarget");
549        assert_eq!(request.compiler_version, "v0.8.30+commit.73712a01");
550    }
551
552    #[test]
553    fn applies_license_type_to_verify_request() {
554        let mut verify_args = VerifyContract::new(
555            Default::default(),
556            "Counter".to_string(),
557            "contract Counter {}".to_string(),
558            "v0.8.23+commit.f704f362".to_string(),
559        );
560
561        apply_license_type(&mut verify_args, Some("13"));
562
563        assert_eq!(verify_args.other.get("licenseType").map(String::as_str), Some("13"));
564    }
565
566    #[test]
567    fn can_extract_etherscan_verify_config() {
568        let temp = tempdir().unwrap();
569        let root = temp.path();
570
571        let config = r#"
572                [profile.default]
573
574                [etherscan]
575                amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
576            "#;
577
578        let toml_file = root.join(Config::FILE_NAME);
579        fs::write(toml_file, config).unwrap();
580
581        let args: VerifyArgs = VerifyArgs::parse_from([
582            "foundry-cli",
583            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
584            "src/Counter.sol:Counter",
585            "--chain",
586            "amoy",
587            "--root",
588            root.as_os_str().to_str().unwrap(),
589        ]);
590
591        let config = args.load_config().unwrap();
592
593        let etherscan = EtherscanVerificationProvider::default();
594        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
595        // Custom URL from foundry.toml should be used
596        assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
597
598        assert!(format!("{client:?}").contains("dummykey"));
599
600        let args: VerifyArgs = VerifyArgs::parse_from([
601            "foundry-cli",
602            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
603            "src/Counter.sol:Counter",
604            "--chain",
605            "amoy",
606            "--verifier-url",
607            "https://verifier-url.com/",
608            "--root",
609            root.as_os_str().to_str().unwrap(),
610        ]);
611
612        let config = args.load_config().unwrap();
613
614        let etherscan = EtherscanVerificationProvider::default();
615        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
616        assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
617        assert!(format!("{client:?}").contains("dummykey"));
618    }
619
620    #[test]
621    fn can_extract_etherscan_v2_verify_config() {
622        let temp = tempdir().unwrap();
623        let root = temp.path();
624
625        let config = r#"
626                [profile.default]
627
628                [etherscan]
629                amoy = { key = "dummykey", chain = 80002, url = "https://amoy.polygonscan.com/" }
630            "#;
631
632        let toml_file = root.join(Config::FILE_NAME);
633        fs::write(toml_file, config).unwrap();
634
635        let args: VerifyArgs = VerifyArgs::parse_from([
636            "foundry-cli",
637            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
638            "src/Counter.sol:Counter",
639            "--verifier",
640            "etherscan",
641            "--chain",
642            "amoy",
643            "--root",
644            root.as_os_str().to_str().unwrap(),
645        ]);
646
647        let config = args.load_config().unwrap();
648
649        let etherscan = EtherscanVerificationProvider::default();
650
651        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
652
653        // Custom URL from foundry.toml should be used
654        assert_eq!(client.etherscan_api_url().as_str(), "https://amoy.polygonscan.com/");
655        assert!(format!("{client:?}").contains("dummykey"));
656
657        let args: VerifyArgs = VerifyArgs::parse_from([
658            "foundry-cli",
659            "0xd8509bee9c9bf012282ad33aba0d87241baf5064",
660            "src/Counter.sol:Counter",
661            "--verifier",
662            "etherscan",
663            "--chain",
664            "amoy",
665            "--verifier-url",
666            "https://verifier-url.com/",
667            "--root",
668            root.as_os_str().to_str().unwrap(),
669        ]);
670
671        let config = args.load_config().unwrap();
672
673        assert_eq!(args.verifier.effective_type(), VerificationProviderType::Etherscan);
674
675        let etherscan = EtherscanVerificationProvider::default();
676        let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap();
677        assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/");
678        assert!(format!("{client:?}").contains("dummykey"));
679    }
680
681    #[tokio::test(flavor = "multi_thread")]
682    async fn fails_on_disabled_cache_and_missing_info() {
683        let temp = tempdir().unwrap();
684        let root = temp.path();
685        let root_path = root.as_os_str().to_str().unwrap();
686
687        let config = r"
688                [profile.default]
689                cache = false
690            ";
691
692        let toml_file = root.join(Config::FILE_NAME);
693        fs::write(toml_file, config).unwrap();
694
695        let address = "0xd8509bee9c9bf012282ad33aba0d87241baf5064";
696        let contract_name = "Counter";
697        let src_dir = "src";
698        fs::create_dir_all(root.join(src_dir)).unwrap();
699        let contract_path = format!("{src_dir}/Counter.sol");
700        fs::write(root.join(&contract_path), "").unwrap();
701
702        // No compiler argument
703        let args = VerifyArgs::parse_from([
704            "foundry-cli",
705            address,
706            &format!("{contract_path}:{contract_name}"),
707            "--root",
708            root_path,
709        ]);
710        let result = args.resolve_context().await;
711        assert!(result.is_err());
712        assert_eq!(
713            result.unwrap_err().to_string(),
714            "If cache is disabled, compiler version must be either provided with `--compiler-version` option or set in foundry.toml"
715        );
716    }
717
718    #[forgetest]
719    async fn respects_path_for_duplicate(prj: _, cmd: _) {
720        prj.add_source("Counter1", "contract Counter {}");
721        prj.add_source("Counter2", "contract Counter {}");
722
723        cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#"
724[COMPILING_FILES] with [SOLC_VERSION]
725...
726[SOLC_VERSION] [ELAPSED]
727Compiler run successful!
728
729"#]]);
730
731        let args = VerifyArgs::parse_from([
732            "foundry-cli",
733            "0x0000000000000000000000000000000000000000",
734            "src/Counter1.sol:Counter",
735            "--root",
736            &prj.root().to_string_lossy(),
737        ]);
738        let context = args.resolve_context().await.unwrap();
739
740        let mut etherscan = EtherscanVerificationProvider::default();
741        etherscan.preflight_verify_check(args, context).await.unwrap();
742    }
743
744    #[forgetest]
745    async fn encodes_constructor_args_from_file(prj: _) {
746        let args_path = prj.root().join("constructor-args.json");
747        for (name, source, input, expected) in [
748            (
749                "Mixed",
750                "contract Mixed { constructor(uint256, string memory, uint256[] memory) {} }",
751                r#"["7", "hello", "[1,2]"]"#,
752                concat!(
753                    "0000000000000000000000000000000000000000000000000000000000000007",
754                    "0000000000000000000000000000000000000000000000000000000000000060",
755                    "00000000000000000000000000000000000000000000000000000000000000a0",
756                    "0000000000000000000000000000000000000000000000000000000000000005",
757                    "68656c6c6f000000000000000000000000000000000000000000000000000000",
758                    "0000000000000000000000000000000000000000000000000000000000000002",
759                    "0000000000000000000000000000000000000000000000000000000000000001",
760                    "0000000000000000000000000000000000000000000000000000000000000002",
761                ),
762            ),
763            ("Empty", "contract Empty { constructor() {} }", "[]", ""),
764        ] {
765            prj.add_source(name, source);
766            fs::write(&args_path, input).unwrap();
767            let args = VerifyArgs::parse_from([
768                "foundry-cli",
769                "0x0000000000000000000000000000000000000000",
770                &format!("src/{name}.sol:{name}"),
771                "--root",
772                &prj.root().to_string_lossy(),
773                "--constructor-args-path",
774                &args_path.to_string_lossy(),
775                "--compiler-version",
776                SOLC_VERSION,
777            ]);
778            let context = args.resolve_context().await.unwrap();
779            let mut etherscan = EtherscanVerificationProvider::default();
780            let encoded = etherscan.constructor_args(&args, &context).await.unwrap();
781            assert_eq!(encoded.as_deref(), Some(expected));
782
783            if name == "Mixed" {
784                fs::write(&args_path, r#"["7"]"#).unwrap();
785                let error = etherscan.constructor_args(&args, &context).await.unwrap_err();
786                assert_eq!(error.to_string(), "encode length mismatch: expected 3 types, got 1");
787            }
788        }
789    }
790}